diff --git a/.configurations/configuration.dsc.yaml b/.configurations/configuration.dsc.yaml index 255da69a5bb..780b1dfa959 100644 --- a/.configurations/configuration.dsc.yaml +++ b/.configurations/configuration.dsc.yaml @@ -12,11 +12,11 @@ properties: - resource: Microsoft.WinGet.DSC/WinGetPackage id: npm directives: - description: Install NodeJS version >=18.15.x and <19 + description: Install NodeJS version 20 allowPrerelease: true settings: id: OpenJS.NodeJS.LTS - version: "18.18.0" + version: "20.14.0" source: winget - resource: NpmDsc/NpmPackage id: yarn diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 31d67db5fac..bc30d7dbe3b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm ADD install-vscode.sh /root/ RUN /root/install-vscode.sh diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index cbcde9bb109..fdd03c400cc 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -4,6 +4,11 @@ "version": "1.0.8", "resolved": "ghcr.io/devcontainers/features/desktop-lite@sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7", "integrity": "sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7" + }, + "ghcr.io/devcontainers/features/rust:1": { + "version": "1.1.3", + "resolved": "ghcr.io/devcontainers/features/rust@sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb", + "integrity": "sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb" } } } \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f5adc4e1c46..75076a0f8b6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,8 @@ "dockerfile": "Dockerfile" }, "features": { - "ghcr.io/devcontainers/features/desktop-lite:1": {} + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "ghcr.io/devcontainers/features/rust:1": {} }, "containerEnv": { "DISPLAY": "" // Allow the Dev Containers extension to set DISPLAY, post-create.sh will add it back in ~/.bashrc and ~/.zshrc if not set. diff --git a/.eslintignore b/.eslintignore index 7bbd3778e90..12da4a432e1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -33,3 +33,4 @@ **/test/unit/assert.js **/test/automation/out/** **/typings/** +!.vscode diff --git a/.eslintplugin/code-no-dangerous-type-assertions.ts b/.eslintplugin/code-no-dangerous-type-assertions.ts new file mode 100644 index 00000000000..eecd4048e43 --- /dev/null +++ b/.eslintplugin/code-no-dangerous-type-assertions.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +export = new class NoDangerousTypeAssertions implements eslint.Rule.RuleModule { + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + // Disable in tests for now + if (context.getFilename().includes('.test')) { + return {}; + } + + return { + // Disallow type assertions on object literals: { ... } or {} as T + ['TSTypeAssertion > ObjectExpression, TSAsExpression > ObjectExpression']: (node: any) => { + const objectNode = node as TSESTree.Node; + + const parent = objectNode.parent as TSESTree.TSTypeAssertion | TSESTree.TSAsExpression; + if ( + // Allow `as const` assertions + (parent.typeAnnotation.type === 'TSTypeReference' && parent.typeAnnotation.typeName.type === 'Identifier' && parent.typeAnnotation.typeName.name === 'const') + + // For also now still allow `any` casts + || (parent.typeAnnotation.type === 'TSAnyKeyword') + ) { + return; + } + + context.report({ + node, + message: "Don't use type assertions for creating objects as this can hide type errors." + }); + }, + }; + } +}; diff --git a/.eslintplugin/code-no-potentially-unsafe-disposables.ts b/.eslintplugin/code-no-potentially-unsafe-disposables.ts new file mode 100644 index 00000000000..69976275051 --- /dev/null +++ b/.eslintplugin/code-no-potentially-unsafe-disposables.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. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; + +/** + * Checks for potentially unsafe usage of `DisposableStore` / `MutableDisposable`. + * + * These have been the source of leaks in the past. + */ +export = new class implements eslint.Rule.RuleModule { + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + function checkVariableDeclaration(inNode: any) { + context.report({ + node: inNode, + message: `Use const for 'DisposableStore' to avoid leaks by accidental reassignment.` + }); + } + + function checkProperty(inNode: any) { + context.report({ + node: inNode, + message: `Use readonly for DisposableStore/MutableDisposable to avoid leaks through accidental reassignment.` + }); + } + + return { + 'VariableDeclaration[kind!="const"] NewExpression[callee.name="DisposableStore"]': checkVariableDeclaration, + + 'PropertyDefinition[readonly!=true][typeAnnotation.typeAnnotation.typeName.name=/DisposableStore|MutableDisposable/]': checkProperty, + 'PropertyDefinition[readonly!=true] NewExpression[callee.name=/DisposableStore|MutableDisposable/]': checkProperty, + }; + } +}; diff --git a/.eslintplugin/code-no-static-self-ref.ts b/.eslintplugin/code-no-static-self-ref.ts new file mode 100644 index 00000000000..7c6e13032ae --- /dev/null +++ b/.eslintplugin/code-no-static-self-ref.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 * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +/** + * WORKAROUND for https://github.com/evanw/esbuild/issues/3823 + */ +export = new class implements eslint.Rule.RuleModule { + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + function checkProperty(inNode: any) { + + const classDeclaration = context.getAncestors().find(node => node.type === 'ClassDeclaration'); + const propertyDefinition = inNode; + + if (!classDeclaration || !classDeclaration.id?.name) { + return; + } + + if (!propertyDefinition.value) { + return; + } + + const classCtor = classDeclaration.body.body.find(node => node.type === 'MethodDefinition' && node.kind === 'constructor') + + if (!classCtor) { + return; + } + + const name = classDeclaration.id.name; + const valueText = context.getSourceCode().getText(propertyDefinition.value) + + if (valueText.includes(name + '.')) { + + if (classCtor.value?.type === 'FunctionExpression' && !classCtor.value.params.find((param: any) => param.type === 'TSParameterProperty' && param.decorators?.length > 0)) { + return + } + + context.report({ + loc: propertyDefinition.value.loc, + message: `Static properties in decorated classes should not reference the class they are defined in. Use 'this' instead. This is a workaround for https://github.com/evanw/esbuild/issues/3823.` + }); + } + + } + + return { + 'PropertyDefinition[static=true]': checkProperty, + }; + } +}; diff --git a/.eslintplugin/code-parameter-properties-must-have-explicit-accessibility.ts b/.eslintplugin/code-parameter-properties-must-have-explicit-accessibility.ts new file mode 100644 index 00000000000..458afd5b0ba --- /dev/null +++ b/.eslintplugin/code-parameter-properties-must-have-explicit-accessibility.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +/** + * Enforces that all parameter properties have an explicit access modifier (public, protected, private). + * + * This catches a common bug where a service is accidentally made public by simply writing: `readonly prop: Foo` + */ +export = new class implements eslint.Rule.RuleModule { + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + function check(inNode: any) { + const node: TSESTree.TSParameterProperty = inNode; + + // For now, only apply to injected services + const firstDecorator = node.decorators?.at(0); + if ( + firstDecorator?.expression.type !== 'Identifier' + || !firstDecorator.expression.name.endsWith('Service') + ) { + return; + } + + if (!node.accessibility) { + context.report({ + node: inNode, + message: 'Parameter properties must have an explicit access modifier.' + }); + } + } + + return { + ['TSParameterProperty']: check, + }; + } +}; diff --git a/.eslintrc.json b/.eslintrc.json index b174b6f348a..8dafc03b087 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -70,7 +70,10 @@ ], "local/code-translation-remind": "warn", "local/code-no-native-private": "warn", + "local/code-parameter-properties-must-have-explicit-accessibility": "warn", "local/code-no-nls-in-standalone-editor": "warn", + "local/code-no-potentially-unsafe-disposables": "warn", + "local/code-no-dangerous-type-assertions": "off", "local/code-no-standalone-editor": "warn", "local/code-no-unexternalized-strings": "warn", "local/code-must-use-super-dispose": "warn", @@ -138,6 +141,14 @@ ] } }, + { + "files": [ + "src/**/*.ts" + ], + "rules": { + "local/code-no-static-self-ref": "warn" + } + }, { "files": [ "src/vs/**/*.test.ts" @@ -148,22 +159,13 @@ { // Files should (only) be removed from the list they adopt the leak detector "exclude": [ - "src/vs/base/test/browser/browser.test.ts", - "src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts", - "src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts", "src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts", - "src/vs/editor/test/common/services/languageService.test.ts", - "src/vs/editor/test/node/classification/typescript.test.ts", "src/vs/platform/configuration/test/common/configuration.test.ts", - "src/vs/platform/extensions/test/common/extensionValidator.test.ts", "src/vs/platform/opener/test/common/opener.test.ts", "src/vs/platform/registry/test/common/platform.test.ts", - "src/vs/platform/remote/test/common/remoteHosts.test.ts", - "src/vs/platform/telemetry/test/browser/1dsAppender.test.ts", "src/vs/platform/workspace/test/common/workspace.test.ts", "src/vs/platform/workspaces/test/electron-main/workspaces.test.ts", "src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts", - "src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts", "src/vs/workbench/api/test/node/extHostTunnelService.test.ts", "src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts", "src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts", @@ -174,7 +176,6 @@ "src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts", "src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts", "src/vs/workbench/services/commands/test/common/commandService.test.ts", - "src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts", "src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts", "src/vs/workbench/test/browser/quickAccess.test.ts" ] @@ -315,6 +316,10 @@ "selector": "BinaryExpression[operator='instanceof'][right.name='MouseEvent']", "message": "Use DOM.isMouseEvent() to support multi-window scenarios." }, + { + "selector": "BinaryExpression[operator='instanceof'][right.name=/^HTML\\w+/]", + "message": "Use DOM.isHTMLElement() and related methods to support multi-window scenarios." + }, { "selector": "BinaryExpression[operator='instanceof'][right.name='KeyboardEvent']", "message": "Use DOM.isKeyboardEvent() to support multi-window scenarios." @@ -648,7 +653,6 @@ "events", "fs", "fs/promises", - "graceful-fs", "http", "https", "minimist", @@ -670,7 +674,7 @@ "vscode-regexpp", "vscode-textmate", "worker_threads", - "@xterm/addon-canvas", + "@xterm/addon-clipboard", "@xterm/addon-image", "@xterm/addon-search", "@xterm/addon-serialize", @@ -751,7 +755,8 @@ "vs/base/~", "vs/base/parts/*/~", "vs/platform/*/~", - "vs/editor/~" + "vs/editor/~", + "@vscode/tree-sitter-wasm" // node module allowed even in /common/ ] }, { @@ -857,7 +862,11 @@ }, // TODO@layers "tas-client-umd", // node module allowed even in /common/ "vscode-textmate", // node module allowed even in /common/ - "@vscode/vscode-languagedetection" // node module allowed even in /common/ + "@vscode/vscode-languagedetection", // node module allowed even in /common/ + { + "when": "hasBrowser", + "pattern": "@xterm/xterm" + } // node module allowed even in /browser/ ] }, { @@ -1016,11 +1025,7 @@ ] }, { - "target": "src/vs/workbench/{workbench.desktop.main.nls.js,workbench.web.main.nls.js}", - "restrictions": [] - }, - { - "target": "src/vs/{loader.d.ts,css.ts,css.build.ts,monaco.d.ts,nls.ts,nls.build.ts,nls.mock.ts}", + "target": "src/vs/{loader.d.ts,css.ts,css.build.ts,monaco.d.ts,nls.ts}", "restrictions": [] }, { @@ -1028,7 +1033,7 @@ "restrictions": [] }, { - "target": "src/{bootstrap-amd.js,bootstrap-fork.js,bootstrap-node.js,bootstrap-window.js,bootstrap.js,cli.js,main.js,server-cli.js,server-main.js}", + "target": "src/{bootstrap-amd.js,bootstrap-fork.js,bootstrap-node.js,bootstrap-window.js,cli.js,main.js,server-cli.js,server-main.js}", "restrictions": [] } ] @@ -1094,7 +1099,9 @@ "local/code-no-runtime-import": [ "error", { - "src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts": ["**/*"] + "src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts": [ + "**/*" + ] } ] } diff --git a/.github/classifier.json b/.github/classifier.json index 44514039e1e..d0a2c778997 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -32,7 +32,7 @@ "debug": {"assign": ["roblourens"]}, "debug-disassembly": {"assign": []}, "dialogs": {"assign": ["sbatten"]}, - "diff-editor": {"assign": ["alexdima"]}, + "diff-editor": {"assign": ["hediet"]}, "dropdown": {"assign": ["lramos15"]}, "editor-api": {"assign": ["alexdima"]}, "editor-autoclosing": {"assign": ["alexdima"]}, @@ -116,7 +116,7 @@ "json": {"assign": ["aeschli"]}, "json-sorting": {"assign": ["aiday-mar"]}, "keybindings": {"assign": ["ulugbekna"]}, - "keybindings-editor": {"assign": ["sandy081"]}, + "keybindings-editor": {"assign": ["ulugbekna"]}, "keyboard-layout": {"assign": ["ulugbekna"]}, "L10N": {"assign": ["TylerLeonhardt", "csigs"]}, "l10n-platform": {"assign": ["TylerLeonhardt"]}, diff --git a/.github/commands.json b/.github/commands.json index 7b04c7475d7..38da97915a2 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -201,28 +201,17 @@ "addLabel": "unreleased" }, { - "type": "label", - "name": "~info-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~info-needed", - "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - "type": "label", - "name": "~version-info-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~version-info-needed", - "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - "type": "label", - "name": "~confirmation-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~confirmation-needed", - "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" + "type": "comment", + "name": "spam", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "close", + "reason": "not_planned", + "addLabel": "invalid" }, { "type": "comment", @@ -525,5 +514,29 @@ "addLabel": "verification-steps-needed", "removeLabel": "~verification-steps-needed", "comment": "Friendly ping! Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." + }, + { + "type": "label", + "name": "~info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~version-info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~version-info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~confirmation-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~confirmation-needed", + "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" } ] diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml deleted file mode 100644 index f914be2f71b..00000000000 --- a/.github/workflows/author-verified.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Author Verified -on: - issues: - types: [closed] - -# also make changes in ./on-label.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - run: npm install --production --prefix ./actions - - name: Run Author Verified - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - uses: ./actions/author-verified - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - requestVerificationComment: "This bug has been fixed in the latest release of [VS Code Insiders](https://code.visualstudio.com/insiders/)!\n\n@${author}, you can help us out by commenting `/verified` if things are now working as expected.\n\nIf things still don't seem right, please ensure you're on version ${commit} of Insiders (today's or later - you can use `Help: About` in the command palette to check), and leave a comment letting us know what isn't working as expected.\n\nHappy Coding!" - releasedLabel: insiders-released - verifiedLabel: verified - authorVerificationRequestedLabel: author-verification-requested diff --git a/.github/workflows/bad-tag.yml b/.github/workflows/bad-tag.yml deleted file mode 100644 index bc964fb0582..00000000000 --- a/.github/workflows/bad-tag.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Bad Tag -on: - create - -jobs: - main: - runs-on: ubuntu-latest - if: github.event.ref == '1.999.0' - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run Bad Tag - uses: ./actions/tag-alert - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - tag-name: '1.999.0' diff --git a/.github/workflows/deep-classifier-assign-monitor.yml b/.github/workflows/deep-classifier-assign-monitor.yml deleted file mode 100644 index cfd9abc374a..00000000000 --- a/.github/workflows/deep-classifier-assign-monitor.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: "Deep Classifier: Assign Monitor" -on: - issues: - types: [assigned] - -jobs: - main: - runs-on: ubuntu-latest - if: ${{ contains(github.event.issue.labels.*.name, 'triage-needed') }} - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: "Run Classifier: Monitor" - uses: ./actions/classifier-deep/monitor - with: - botName: VSCodeTriageBot - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml deleted file mode 100644 index 71954a68c10..00000000000 --- a/.github/workflows/deep-classifier-runner.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: "Deep Classifier: Runner" - -permissions: - id-token: write - contents: read - -on: - schedule: - - cron: 0 * * * * - workflow_dispatch: - repository_dispatch: - types: [trigger-deep-classifier-runner] - -jobs: - main: - runs-on: ubuntu-latest - environment: main - steps: - - uses: azure/login@v1 - with: - client-id: ${{ vars.AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - allow-no-subscriptions: true - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Install Additional Dependencies - # Pulls in a bunch of other packages that arent needed for the rest of the actions - run: npm install @azure/storage-blob@12.1.1 mongodb@2.2.31 - - name: "Run Classifier: Scraper" - uses: ./actions/classifier-deep/apply/fetch-sources - with: - # slightly overlapping to protect against issues slipping through the cracks if a run is delayed - until: 5 - excludeLabels: feature-request|testplan-item - configPath: classifier - blobContainerName: vscode-issue-classifier - blobStorageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - - name: Set up Python 3.7 - uses: actions/setup-python@v5 - with: - python-version: 3.7 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --upgrade numpy==1.20.0 scipy==1.6.0 scikit-learn==0.24.1 joblib==1.0.0 nltk==3.5 simpletransformers==0.51.16 torch==1.7.1 torchvision==0.8.2 - - name: "Run Classifier: Generator" - run: python ./actions/classifier-deep/apply/generate-labels/main.py - - name: "Run Classifier: Labeler" - uses: ./actions/classifier-deep/apply/apply-labels - with: - configPath: classifier - allowLabels: "info-needed|new release|error-telemetry|*english-please|translation-required" - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml deleted file mode 100644 index e21061549d9..00000000000 --- a/.github/workflows/deep-classifier-scraper.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Deep Classifier: Scraper" -on: - schedule: - - cron: 0 0 15 * * # 15th of the month - workflow_dispatch: - repository_dispatch: - types: [trigger-deep-classifier-scraper] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Install Additional Dependencies - # Pulls in a bunch of other packages that arent needed for the rest of the actions - run: npm install @azure/storage-blob@12.1.1 - - name: "Run Classifier: Scraper" - uses: ./actions/classifier-deep/train/fetch-issues - with: - blobContainerName: vscode-issue-classifier - blobStorageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/deep-classifier-unassign-monitor.yml b/.github/workflows/deep-classifier-unassign-monitor.yml deleted file mode 100644 index d0e14e936c2..00000000000 --- a/.github/workflows/deep-classifier-unassign-monitor.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: "Deep Classifier: Unassign Monitor" -on: - issues: - types: [unassigned] - -jobs: - main: - runs-on: ubuntu-latest - if: ${{ ! contains(github.event.issue.labels.*.name, 'triage-needed') }} - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: "Run Classifier: Monitor" - uses: ./actions/classifier-deep/monitor - with: - botName: VSCodeTriageBot - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml deleted file mode 100644 index 9e04d6d549c..00000000000 --- a/.github/workflows/english-please.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: English Please -on: - issues: - types: [edited] - -# also make changes in ./on-label.yml and ./on-open.yml -jobs: - main: - runs-on: ubuntu-latest - if: contains(github.event.issue.labels.*.name, '*english-please') - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run English Please - uses: ./actions/english-please - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - cognitiveServicesAPIKey: ${{secrets.AZURE_TEXT_TRANSLATOR_KEY}} - nonEnglishLabel: "*english-please" - needsMoreInfoLabel: "info-needed" - translatorRequestedLabelPrefix: "translation-required-" - translatorRequestedLabelColor: "c29cff" diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml deleted file mode 100644 index 83c0a9705c5..00000000000 --- a/.github/workflows/feature-request.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Feature Request Manager -on: - repository_dispatch: - types: [trigger-feature-request-manager] - issues: - types: [milestoned] - schedule: - - cron: 20 2 * * * # 4:20am Zurich - -# also make changes in ./on-label.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - run: npm install --production --prefix ./actions - - name: Run Feature Request Manager - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - uses: ./actions/feature-request - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - candidateMilestoneID: 107 - candidateMilestoneName: Backlog Candidates - backlogMilestoneID: 8 - featureRequestLabel: feature-request - upvotesRequired: 20 - numCommentsOverride: 20 - initComment: "This feature request is now a candidate for our backlog. The community has 60 days to [upvote](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnComment: "This feature request has not yet received the 20 community [upvotes](https://github.com/microsoft/vscode/wiki/Issues-Triaging#up-voting-a-feature-request) it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - rejectComment: ":slightly_frowning_face: In the last 60 days, this feature request has received less than 20 community upvotes and we closed it. Still a big Thank You to you for taking the time to create this issue! To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnDays: 10 - closeDays: 60 - milestoneDelaySeconds: 60 diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml deleted file mode 100644 index f7392dc24a8..00000000000 --- a/.github/workflows/latest-release-monitor.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Latest Release Monitor -on: - schedule: - - cron: 0/5 * * * * - repository_dispatch: - types: [trigger-latest-release-monitor] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Install Storage Module - run: npm install @azure/storage-blob@12.1.1 - - name: Run Latest Release Monitor - uses: ./actions/latest-release-monitor - with: - storageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml deleted file mode 100644 index 5860349a437..00000000000 --- a/.github/workflows/locker.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Locker -on: - schedule: - - cron: 20 23 * * * # 4:20pm Redmond - repository_dispatch: - types: [trigger-locker] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run Locker - uses: ./actions/locker - with: - daysSinceClose: 45 - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - daysSinceUpdate: 3 - ignoredLabel: "*out-of-scope,accessibility" - ignoreLabelUntil: "author-verification-requested" - labelUntil: "verified" diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml deleted file mode 100644 index 8db8a4246a3..00000000000 --- a/.github/workflows/needs-more-info-closer.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: info-needed Closer -on: - schedule: - - cron: 20 11 * * * # 4:20am Redmond - repository_dispatch: - types: [trigger-needs-more-info] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run info-needed Closer - uses: ./actions/needs-more-info-closer - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - label: info-needed - closeDays: 7 - additionalTeam: "cleidigh|usernamehw|gjsjohnmurray|IllusionMH" - closeComment: "This issue has been closed automatically because it needs more information and has not had recent activity. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" - pingDays: 80 - pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." diff --git a/.github/workflows/on-comment.yml b/.github/workflows/on-comment.yml deleted file mode 100644 index 089aa77c1e7..00000000000 --- a/.github/workflows/on-comment.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: On Comment -on: - issue_comment: - types: [created] - -# also make changes in ./on-label.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run Commands - uses: ./actions/commands - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - config-path: commands - - name: "Run Release Pipeline Labeler" - uses: ./actions/release-pipeline - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - notYetReleasedLabel: unreleased - insidersReleasedLabel: insiders-released diff --git a/.github/workflows/on-label.yml b/.github/workflows/on-label.yml deleted file mode 100644 index bf563734017..00000000000 --- a/.github/workflows/on-label.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: On Label -on: - issues: - types: [labeled] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - # source of truth in ./author-verified.yml - - name: Run Author Verified - if: contains(github.event.issue.labels.*.name, 'author-verification-requested') && contains(github.event.issue.labels.*.name, 'insiders-released') - uses: ./actions/author-verified - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - requestVerificationComment: "This bug has been fixed in the latest release of [VS Code Insiders](https://code.visualstudio.com/insiders/)!\n\n@${author}, you can help us out by commenting `/verified` if things are now working as expected.\n\nIf things still don't seem right, please ensure you're on version ${commit} of Insiders (today's or later - you can use `Help: About` in the command palette to check), and leave a comment letting us know what isn't working as expected.\n\nHappy Coding!" - releasedLabel: insiders-released - verifiedLabel: verified - authorVerificationRequestedLabel: author-verification-requested - - - # also make changes in ./on-comment.yml - - name: Run Commands - uses: ./actions/commands - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - config-path: commands - - # source of truth in ./feature-request.yml - - name: Run Feature Request Manager - if: contains(github.event.issue.labels.*.name, 'feature-request') - uses: ./actions/feature-request - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - candidateMilestoneID: 107 - candidateMilestoneName: Backlog Candidates - backlogMilestoneID: 8 - featureRequestLabel: feature-request - upvotesRequired: 20 - numCommentsOverride: 20 - initComment: "This feature request is now a candidate for our backlog. The community has 60 days to upvote the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnComment: "This feature request has not yet received the 20 community upvotes it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding" - acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - rejectComment: ":slightly_frowning_face: In the last 60 days, this feature request has received less than 20 community upvotes and we closed it. Still a big Thank You to you for taking the time to create this issue! To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!" - warnDays: 10 - closeDays: 60 - milestoneDelaySeconds: 60 - - # source of truth in ./test-plan-item-validator.yml - - name: Run Test Plan Item Validator - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - uses: ./actions/test-plan-item-validator - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - refLabel: on-testplan - label: testplan-item - invalidLabel: invalid-testplan-item - comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. - - # source of truth in ./english-please.yml - - name: Run English Please - if: contains(github.event.issue.labels.*.name, '*english-please') - uses: ./actions/english-please - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - cognitiveServicesAPIKey: ${{secrets.AZURE_TEXT_TRANSLATOR_KEY}} - nonEnglishLabel: "*english-please" - needsMoreInfoLabel: "info-needed" - translatorRequestedLabelPrefix: "translation-required-" - translatorRequestedLabelColor: "c29cff" diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml deleted file mode 100644 index 361ac11b946..00000000000 --- a/.github/workflows/on-open.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: On Open -on: - issues: - types: [opened] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Run CopyCat (VSCodeTriageBot/testissues) - uses: ./actions/copycat - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - owner: VSCodeTriageBot - repo: testissues - - - name: Run New Release - uses: ./actions/new-release - with: - label: new release - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - labelColor: "006b75" - labelDescription: Issues found in a recent release of VS Code - oldVersionMessage: "Thanks for creating this issue! It looks like you may be using an old version of VS Code, the latest stable release is {currentVersion}. Please try upgrading to the latest version and checking whether this issue remains.\n\nHappy Coding!" - days: 5 - - - name: Run Clipboard Labeler - uses: ./actions/regex-labeler - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - label: "invalid" - mustNotMatch: "^We have written the needed data into your clipboard because it was too large to send\\. Please paste\\.$" - comment: "It looks like you're using the VS Code Issue Reporter but did not paste the text generated into the created issue. We've closed this issue, please open a new one containing the text we placed in your clipboard.\n\nHappy Coding!" - - - name: Run Clipboard Labeler (Chinese) - uses: ./actions/regex-labeler - with: - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - label: "invalid" - mustNotMatch: "^所需的数据太大,无法直接发送。我们已经将其写入剪贴板,请粘贴。$" - comment: "看起来您正在使用 VS Code 问题报告程序,但是没有将生成的文本粘贴到创建的问题中。我们将关闭这个问题,请使用剪贴板中的内容创建一个新的问题。\n\n祝您使用愉快!" - - # source of truth in ./english-please.yml - - name: Run English Please - uses: ./actions/english-please - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - cognitiveServicesAPIKey: ${{secrets.AZURE_TEXT_TRANSLATOR_KEY}} - nonEnglishLabel: "*english-please" - needsMoreInfoLabel: "info-needed" - translatorRequestedLabelPrefix: "translation-required-" - translatorRequestedLabelColor: "c29cff" - # source of truth in ./test-plan-item-validator.yml - - name: Run Test Plan Item Validator - uses: ./actions/test-plan-item-validator - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - refLabel: on-testplan - label: testplan-item - invalidLabel: invalid-testplan-item - comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. - diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml deleted file mode 100644 index 87e188a02ab..00000000000 --- a/.github/workflows/release-pipeline-labeler.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "Release Pipeline Labeler" -on: - issues: - types: [closed, reopened] - repository_dispatch: - types: [released-insider] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - ref: stable - path: ./actions - - name: Checkout Repo - if: github.event_name != 'issues' - uses: actions/checkout@v4 - with: - path: ./repo - fetch-depth: 0 - - name: Install Actions - run: npm install --production --prefix ./actions - - name: "Run Release Pipeline Labeler" - uses: ./actions/release-pipeline - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - notYetReleasedLabel: unreleased - insidersReleasedLabel: insiders-released diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index d463a0e2eca..d29ea6c58da 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -16,4 +16,4 @@ jobs: - name: 'Run vscode-telemetry-extractor' run: 'npx --package=@vscode/telemetry-extractor --yes vscode-telemetry-extractor -s .' env: - GITHUB_TOKEN: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml deleted file mode 100644 index 117eaf6908a..00000000000 --- a/.github/workflows/test-plan-item-validator.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Test Plan Item Validator -on: - issues: - types: [edited] - -# also edit in ./on-label.yml and ./on-open.yml -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - uses: actions/checkout@v4 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - run: npm install --production --prefix ./actions - - name: Run Test Plan Item Validator - if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') - uses: ./actions/test-plan-item-validator - with: - token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - refLabel: on-testplan - label: testplan-item - invalidLabel: invalid-testplan-item - comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. diff --git a/.nvmrc b/.nvmrc index a9d087399d7..48b14e6b2b5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.19.0 +20.14.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3d58135095b..737efece5a4 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,7 +6,6 @@ "editorconfig.editorconfig", "github.vscode-pull-request-github", "ms-vscode.vscode-github-issue-notebooks", - "ms-vscode.vscode-selfhost-test-provider", "ms-vscode.extension-test-runner", "jrieken.vscode-pr-pinger" ] diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json new file mode 100644 index 00000000000..deb2c584c47 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--enable-proposed-api=ms-vscode.vscode-selfhost-test-provider"], + "name": "Launch Extension", + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "request": "launch", + "type": "extensionHost" + } + ] +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json new file mode 100644 index 00000000000..e4429caeee4 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/icon.png b/.vscode/extensions/vscode-selfhost-test-provider/icon.png new file mode 100644 index 00000000000..46bf5e4d3bc Binary files /dev/null and b/.vscode/extensions/vscode-selfhost-test-provider/icon.png differ diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json new file mode 100644 index 00000000000..3548b00ba81 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -0,0 +1,90 @@ +{ + "name": "vscode-selfhost-test-provider", + "displayName": "VS Code Selfhost Test Provider", + "description": "Test provider for the VS Code project", + "enabledApiProposals": [ + "testObserver", + "testRelatedCode", + "attributableCoverage" + ], + "engines": { + "vscode": "^1.88.0" + }, + "contributes": { + "commands": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "title": "Update Snapshot", + "category": "Testing", + "icon": "$(merge)" + }, + { + "command": "selfhost-test-provider.openFailureLog", + "title": "Open Selfhost Failure Logs", + "category": "Testing", + "icon": "$(merge)" + } + ], + "menus": { + "commandPalette": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "when": "false" + } + ], + "testing/message/context": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "group": "inline@1", + "when": "testMessage == isSelfhostSnapshotMessage && !testResultOutdated" + } + ], + "testing/message/content": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "when": "testMessage == isSelfhostSnapshotMessage && !testResultOutdated" + } + ] + } + }, + "icon": "icon.png", + "version": "0.4.0", + "publisher": "ms-vscode", + "categories": [ + "Other" + ], + "activationEvents": [ + "workspaceContains:src/vs/loader.js" + ], + "workspaceTrust": { + "request": "onDemand", + "description": "Trust is required to execute tests in the workspace." + }, + "main": "./out/extension.js", + "prettier": { + "printWidth": 100, + "singleQuote": true, + "tabWidth": 2, + "arrowParens": "avoid" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + }, + "license": "MIT", + "scripts": { + "compile": "gulp compile-extension:vscode-selfhost-test-provider", + "watch": "gulp watch-extension:vscode-selfhost-test-provider", + "test": "npx mocha --ui tdd 'out/*.test.js'" + }, + "devDependencies": { + "@types/mocha": "^10.0.6", + "@types/node": "20.x" + }, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "ansi-styles": "^5.2.0", + "cockatiel": "^3.1.3", + "istanbul-to-vscode": "^2.0.1" + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts new file mode 100644 index 00000000000..3fff7c5b637 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IstanbulCoverageContext } from 'istanbul-to-vscode'; +import * as vscode from 'vscode'; +import { SearchStrategy, SourceLocationMapper, SourceMapStore } from './testOutputScanner'; +import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; + +export const istanbulCoverageContext = new IstanbulCoverageContext(); + +/** + * Tracks coverage in per-script coverage mode. There are two modes of coverage + * in this extension: generic istanbul reports, and reports from the runtime + * sent before and after each test case executes. This handles the latter. + */ +export class PerTestCoverageTracker { + private readonly scripts = new Map(); + + constructor(private readonly maps: SourceMapStore) { } + + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + const script = this.scripts.get(coverage.scriptId); + if (script) { + return script.add(coverage, test); + } + // ignore internals and node_modules + if (!coverage.url.startsWith('file://') || coverage.url.includes('node_modules')) { + return; + } + if (!coverage.source) { + throw new Error('expected to have source the first time a script is seen'); + } + + const src = new Script(vscode.Uri.parse(coverage.url), coverage.source, this.maps); + this.scripts.set(coverage.scriptId, src); + src.add(coverage, test); + } + + public async report(run: vscode.TestRun) { + await Promise.all(Array.from(this.scripts.values()).map(s => s.report(run))); + } +} + +class Script { + private converter: OffsetToPosition; + + /** Tracking the overall coverage for the file */ + private overall = new ScriptCoverageTracker(); + /** Range tracking per-test item */ + private readonly perItem = new Map(); + + constructor( + public readonly uri: vscode.Uri, + source: string, + private readonly maps: SourceMapStore + ) { + this.converter = new OffsetToPosition(source); + } + + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + this.overall.add(coverage); + if (test) { + const p = new ScriptCoverageTracker(); + p.add(coverage); + this.perItem.set(test, p); + } + } + + public async report(run: vscode.TestRun) { + const mapper = await this.maps.getSourceLocationMapper(this.uri.toString()); + const originalUri = (await this.maps.getSourceFile(this.uri.toString())) || this.uri; + run.addCoverage(this.overall.report(originalUri, this.converter, mapper, this.perItem)); + } +} + +class ScriptCoverageTracker { + private coverage = new RangeCoverageTracker(); + + public add(coverage: IScriptCoverage) { + for (const range of RangeCoverageTracker.initializeBlocks(coverage.functions)) { + this.coverage.setCovered(range.start, range.end, range.covered); + } + } + + public *toDetails( + uri: vscode.Uri, + convert: OffsetToPosition, + mapper: SourceLocationMapper | undefined, + ) { + for (const range of this.coverage) { + if (range.start === range.end) { + continue; + } + + const startCov = convert.toLineColumn(range.start); + let start = new vscode.Position(startCov.line, startCov.column); + + const endCov = convert.toLineColumn(range.end); + let end = new vscode.Position(endCov.line, endCov.column); + if (mapper) { + const startMap = mapper(start.line, start.character, SearchStrategy.FirstAfter); + const endMap = startMap && mapper(end.line, end.character, SearchStrategy.FirstBefore); + if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) { + continue; + } + start = startMap.range.start; + end = endMap.range.end; + } + + for (let i = start.line; i <= end.line; i++) { + yield new vscode.StatementCoverage( + range.covered, + new vscode.Range( + new vscode.Position(i, i === start.line ? start.character : 0), + new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER) + ) + ); + } + } + } + + /** + * Generates the script's coverage for the test run. + * + * If a source location mapper is given, it assumes the `uri` is the mapped + * URI, and that any unmapped locations/outside the URI should be ignored. + */ + public report( + uri: vscode.Uri, + convert: OffsetToPosition, + mapper: SourceLocationMapper | undefined, + items: Map, + ): V8CoverageFile { + const file = new V8CoverageFile(uri, items, convert, mapper); + for (const detail of this.toDetails(uri, convert, mapper)) { + file.add(detail); + } + + return file; + } +} + +export class V8CoverageFile extends vscode.FileCoverage2 { + public details: vscode.StatementCoverage[] = []; + + constructor( + uri: vscode.Uri, + private readonly perTest: Map, + private readonly convert: OffsetToPosition, + private readonly mapper: SourceLocationMapper | undefined, + ) { + super(uri, { covered: 0, total: 0 }, undefined, undefined, [...perTest.keys()]); + } + + public add(detail: vscode.StatementCoverage) { + this.details.push(detail); + this.statementCoverage.total++; + if (detail.executed) { + this.statementCoverage.covered++; + } + } + + public testDetails(test: vscode.TestItem): vscode.FileCoverageDetail[] { + const t = this.perTest.get(test); + return t ? [...t.toDetails(this.uri, this.convert, this.mapper)] : []; + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts new file mode 100644 index 00000000000..fd5c087c074 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Debounces the function call for an interval. + */ +export function debounce(duration: number, fn: () => void): (() => void) & { clear: () => void } { + let timeout: NodeJS.Timeout | void; + const debounced = () => { + if (timeout !== undefined) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + timeout = undefined; + fn(); + }, duration); + }; + + debounced.clear = () => { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + }; + + return debounced; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts new file mode 100644 index 00000000000..2732ef3b3f6 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -0,0 +1,360 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { randomBytes } from 'crypto'; +import { tmpdir } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { V8CoverageFile } from './coverageProvider'; +import { FailingDeepStrictEqualAssertFixer } from './failingDeepStrictEqualAssertFixer'; +import { FailureTracker } from './failureTracker'; +import { registerSnapshotUpdate } from './snapshot'; +import { scanTestOutput } from './testOutputScanner'; +import { + TestCase, + TestFile, + clearFileDiagnostics, + guessWorkspaceFolder, + itemData, +} from './testTree'; +import { BrowserTestRunner, PlatformTestRunner, VSCodeTestRunner } from './vscodeTestRunner'; +import { ImportGraph } from './importGraph'; + +const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts'; + +const getWorkspaceFolderForTestFile = (uri: vscode.Uri) => + (uri.path.endsWith('.test.ts') || uri.path.endsWith('.integrationTest.ts')) && + uri.path.includes('/src/vs/') + ? vscode.workspace.getWorkspaceFolder(uri) + : undefined; + +const browserArgs: [name: string, arg: string][] = [ + ['Chrome', 'chromium'], + ['Firefox', 'firefox'], + ['Webkit', 'webkit'], +]; + +type FileChangeEvent = { uri: vscode.Uri; removed: boolean }; + +export async function activate(context: vscode.ExtensionContext) { + const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests'); + const fileChangedEmitter = new vscode.EventEmitter(); + + context.subscriptions.push(vscode.tests.registerTestFollowupProvider({ + async provideFollowup(_result, test, taskIndex, messageIndex, _token) { + return [{ + title: '$(sparkle) Fix with Copilot', + command: 'github.copilot.tests.fixTestFailure', + arguments: [{ source: 'peekFollowup', test, message: test.taskStates[taskIndex].messages[messageIndex] }] + }]; + }, + })); + + let initialWatchPromise: Promise | undefined; + const resolveHandler = async (test?: vscode.TestItem) => { + if (!test) { + if (!initialWatchPromise) { + initialWatchPromise = startWatchingWorkspace(ctrl, fileChangedEmitter); + context.subscriptions.push(await initialWatchPromise); + } else { + await initialWatchPromise; + } + return; + } + + const data = itemData.get(test); + if (data instanceof TestFile) { + // No need to watch this, updates will be triggered on file changes + // either by the text document or file watcher. + await data.updateFromDisk(ctrl, test); + } + }; + + ctrl.resolveHandler = resolveHandler; + + guessWorkspaceFolder().then(folder => { + if (!folder) { + return; + } + + const graph = new ImportGraph( + folder.uri, async () => { + await resolveHandler(); + return [...ctrl.items].map(([, item]) => item); + }, uri => ctrl.items.get(uri.toString().toLowerCase())); + ctrl.relatedCodeProvider = graph; + + context.subscriptions.push( + new FailureTracker(context, folder.uri.fsPath), + fileChangedEmitter.event(e => graph.didChange(e.uri, e.removed)), + ); + }); + + const createRunHandler = ( + runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner }, + kind: vscode.TestRunProfileKind, + args: string[] = [] + ) => { + const doTestRun = async ( + req: vscode.TestRunRequest, + cancellationToken: vscode.CancellationToken + ) => { + const folder = await guessWorkspaceFolder(); + if (!folder) { + return; + } + + const runner = new runnerCtor(folder); + const map = await getPendingTestMap(ctrl, req.include ?? gatherTestItems(ctrl.items)); + const task = ctrl.createTestRun(req); + for (const test of map.values()) { + task.enqueued(test); + } + + let coverageDir: string | undefined; + let currentArgs = args; + if (kind === vscode.TestRunProfileKind.Coverage) { + // todo: browser runs currently don't support per-test coverage + if (args.includes('--browser')) { + coverageDir = path.join( + tmpdir(), + `vscode-test-coverage-${randomBytes(8).toString('hex')}` + ); + currentArgs = [ + ...currentArgs, + '--coverage', + '--coveragePath', + coverageDir, + '--coverageFormats', + 'json', + ]; + } else { + currentArgs = [...currentArgs, '--per-test-coverage']; + } + } + + return await scanTestOutput( + map, + task, + kind === vscode.TestRunProfileKind.Debug + ? await runner.debug(task, currentArgs, req.include) + : await runner.run(currentArgs, req.include), + coverageDir, + cancellationToken + ); + }; + + return async (req: vscode.TestRunRequest, cancellationToken: vscode.CancellationToken) => { + if (!req.continuous) { + return doTestRun(req, cancellationToken); + } + + const queuedFiles = new Set(); + let debounced: NodeJS.Timeout | undefined; + + const listener = fileChangedEmitter.event(({ uri, removed }) => { + clearTimeout(debounced); + + if (req.include && !req.include.some(i => i.uri?.toString() === uri.toString())) { + return; + } + + if (removed) { + queuedFiles.delete(uri.toString()); + } else { + queuedFiles.add(uri.toString()); + } + + debounced = setTimeout(() => { + const include = + req.include?.filter(t => t.uri && queuedFiles.has(t.uri?.toString())) ?? + [...queuedFiles] + .map(f => getOrCreateFile(ctrl, vscode.Uri.parse(f))) + .filter((f): f is vscode.TestItem => !!f); + queuedFiles.clear(); + + doTestRun( + new vscode.TestRunRequest(include, req.exclude, req.profile, true), + cancellationToken + ); + }, 1000); + }); + + cancellationToken.onCancellationRequested(() => { + clearTimeout(debounced); + listener.dispose(); + }); + }; + }; + + ctrl.createRunProfile( + 'Run in Electron', + vscode.TestRunProfileKind.Run, + createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Run), + true, + undefined, + true + ); + + ctrl.createRunProfile( + 'Debug in Electron', + vscode.TestRunProfileKind.Debug, + createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Debug), + true, + undefined, + true + ); + + const coverage = ctrl.createRunProfile( + 'Coverage in Electron', + vscode.TestRunProfileKind.Coverage, + createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Coverage), + true, + undefined, + true + ); + + coverage.loadDetailedCoverage = async (_run, coverage) => coverage instanceof V8CoverageFile ? coverage.details : []; + coverage.loadDetailedCoverageForTest = async (_run, coverage, test) => coverage instanceof V8CoverageFile ? coverage.testDetails(test) : []; + + for (const [name, arg] of browserArgs) { + const cfg = ctrl.createRunProfile( + `Run in ${name}`, + vscode.TestRunProfileKind.Run, + createRunHandler(BrowserTestRunner, vscode.TestRunProfileKind.Run, [' --browser', arg]), + undefined, + undefined, + true + ); + + cfg.configureHandler = () => vscode.window.showInformationMessage(`Configuring ${name}`); + + ctrl.createRunProfile( + `Debug in ${name}`, + vscode.TestRunProfileKind.Debug, + createRunHandler(BrowserTestRunner, vscode.TestRunProfileKind.Debug, [ + '--browser', + arg, + '--debug-browser', + ]), + undefined, + undefined, + true + ); + } + + function updateNodeForDocument(e: vscode.TextDocument) { + const node = getOrCreateFile(ctrl, e.uri); + const data = node && itemData.get(node); + if (data instanceof TestFile) { + data.updateFromContents(ctrl, e.getText(), node!); + } + } + + for (const document of vscode.workspace.textDocuments) { + updateNodeForDocument(document); + } + + context.subscriptions.push( + ctrl, + fileChangedEmitter.event(({ uri, removed }) => { + if (!removed) { + const node = getOrCreateFile(ctrl, uri); + if (node) { + ctrl.invalidateTestResults(); + } + } + }), + vscode.workspace.onDidOpenTextDocument(updateNodeForDocument), + vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document)), + registerSnapshotUpdate(ctrl), + new FailingDeepStrictEqualAssertFixer() + ); +} + +export function deactivate() { + // no-op +} + +function getOrCreateFile( + controller: vscode.TestController, + uri: vscode.Uri +): vscode.TestItem | undefined { + const folder = getWorkspaceFolderForTestFile(uri); + if (!folder) { + return undefined; + } + + const data = new TestFile(uri, folder); + const existing = controller.items.get(data.getId()); + if (existing) { + return existing; + } + + const file = controller.createTestItem(data.getId(), data.getLabel(), uri); + controller.items.add(file); + file.canResolveChildren = true; + itemData.set(file, data); + + return file; +} + +function gatherTestItems(collection: vscode.TestItemCollection) { + const items: vscode.TestItem[] = []; + collection.forEach(item => items.push(item)); + return items; +} + +async function startWatchingWorkspace( + controller: vscode.TestController, + fileChangedEmitter: vscode.EventEmitter +) { + const workspaceFolder = await guessWorkspaceFolder(); + if (!workspaceFolder) { + return new vscode.Disposable(() => undefined); + } + + const pattern = new vscode.RelativePattern(workspaceFolder, TEST_FILE_PATTERN); + const watcher = vscode.workspace.createFileSystemWatcher(pattern); + + watcher.onDidCreate(uri => { + getOrCreateFile(controller, uri); + fileChangedEmitter.fire({ removed: false, uri }); + }); + watcher.onDidChange(uri => fileChangedEmitter.fire({ removed: false, uri })); + watcher.onDidDelete(uri => { + fileChangedEmitter.fire({ removed: true, uri }); + clearFileDiagnostics(uri); + controller.items.delete(uri.toString()); + }); + + for (const file of await vscode.workspace.findFiles(pattern)) { + getOrCreateFile(controller, file); + } + + return watcher; +} + +async function getPendingTestMap(ctrl: vscode.TestController, tests: Iterable) { + const queue = [tests]; + const titleMap = new Map(); + while (queue.length) { + for (const item of queue.pop()!) { + const data = itemData.get(item); + if (data instanceof TestFile) { + if (!data.hasBeenRead) { + await data.updateFromDisk(ctrl, item); + } + queue.push(gatherTestItems(item.children)); + } else if (data instanceof TestCase) { + titleMap.set(data.fullName, item); + } else { + queue.push(gatherTestItems(item.children)); + } + } + } + + return titleMap; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts new file mode 100644 index 00000000000..17e65cbce50 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts @@ -0,0 +1,254 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + commands, + Disposable, + languages, + Position, + Range, + TestMessage, + TestResultSnapshot, + TestRunResult, + tests, + TextDocument, + Uri, + workspace, + WorkspaceEdit, +} from 'vscode'; +import { memoizeLast } from './memoize'; +import { getTestMessageMetadata } from './metadata'; + +const enum Constants { + FixCommandId = 'selfhost-test.fix-test', +} + +export class FailingDeepStrictEqualAssertFixer { + private disposables: Disposable[] = []; + + constructor() { + this.disposables.push( + commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => { + const document = await workspace.openTextDocument(uri); + + const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position); + if (!failingAssertion) { + return; + } + + const expectedValueNode = failingAssertion.assertion.expectedValue; + if (!expectedValueNode) { + return; + } + + const start = document.positionAt(expectedValueNode.getStart()); + const end = document.positionAt(expectedValueNode.getEnd()); + + const edit = new WorkspaceEdit(); + edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue)); + await workspace.applyEdit(edit); + }) + ); + + this.disposables.push( + languages.registerCodeActionsProvider('typescript', { + provideCodeActions: (document, range) => { + const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start); + if (!failingAssertion) { + return undefined; + } + + return [ + { + title: 'Fix Expected Value', + command: Constants.FixCommandId, + arguments: [document.uri, range.start], + }, + ]; + }, + }) + ); + } + + dispose() { + for (const d of this.disposables) { + d.dispose(); + } + } +} + +const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i; + +const tsPrinter = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + +const formatJsonValue = (value: unknown) => { + if (typeof value !== 'object') { + return JSON.stringify(value); + } + + const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true); + const outerExpression = src.statements[0] as ts.ExpressionStatement; + const parenExpression = outerExpression.expression as ts.ParenthesizedExpression; + + const unquoted = ts.transform(parenExpression, [ + context => (node: ts.Node) => { + const visitor = (node: ts.Node): ts.Node => + ts.isPropertyAssignment(node) && + ts.isStringLiteralLike(node.name) && + identifierLikeRe.test(node.name.text) + ? ts.factory.createPropertyAssignment( + ts.factory.createIdentifier(node.name.text), + ts.visitNode(node.initializer, visitor) as ts.Expression + ) + : ts.isStringLiteralLike(node) && node.text === '[undefined]' + ? ts.factory.createIdentifier('undefined') + : ts.visitEachChild(node, visitor, context); + + return ts.visitNode(node, visitor); + }, + ]); + + return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src); +}; + +/** Parses the source file, memoizing the last document so cursor moves are efficient */ +const parseSourceFile = memoizeLast((text: string) => + ts.createSourceFile('', text, ts.ScriptTarget.ES5, true) +); + +const assertionFailureMessageRe = /^Expected values to be strictly (deep-)?equal:/; + +/** Gets information about the failing assertion at the poisition, if any. */ +function detectFailingDeepStrictEqualAssertion( + document: TextDocument, + position: Position +): { assertion: StrictEqualAssertion; actualJSONValue: unknown } | undefined { + const sf = parseSourceFile(document.getText()); + const offset = document.offsetAt(position); + const assertion = StrictEqualAssertion.atPosition(sf, offset); + if (!assertion) { + return undefined; + } + + const startLine = document.positionAt(assertion.offsetStart).line; + const messages = getAllTestStatusMessagesAt(document.uri, startLine); + const strictDeepEqualMessage = messages.find(m => + assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value) + ); + + if (!strictDeepEqualMessage) { + return undefined; + } + + const metadata = getTestMessageMetadata(strictDeepEqualMessage); + if (!metadata) { + return undefined; + } + + return { + assertion: assertion, + actualJSONValue: metadata.actualValue, + }; +} + +class StrictEqualAssertion { + /** + * Extracts the assertion at the current node, if it is one. + */ + public static fromNode(node: ts.Node): StrictEqualAssertion | undefined { + if (!ts.isCallExpression(node)) { + return undefined; + } + + const expr = node.expression.getText(); + if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') { + return undefined; + } + + return new StrictEqualAssertion(node); + } + + /** + * Gets the equals assertion at the given offset in the file. + */ + public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined { + let node = findNodeAt(sf, offset); + + while (node.parent) { + const obj = StrictEqualAssertion.fromNode(node); + if (obj) { + return obj; + } + node = node.parent; + } + + return undefined; + } + + constructor(private readonly expression: ts.CallExpression) { } + + /** Gets the expected value */ + public get expectedValue(): ts.Expression | undefined { + return this.expression.arguments[1]; + } + + /** Gets the position of the assertion expression. */ + public get offsetStart(): number { + return this.expression.getStart(); + } +} + +function findNodeAt(parent: ts.Node, offset: number): ts.Node { + for (const child of parent.getChildren()) { + if (child.getStart() <= offset && offset <= child.getEnd()) { + return findNodeAt(child, offset); + } + } + return parent; +} + +function getAllTestStatusMessagesAt(uri: Uri, lineNumber: number): TestMessage[] { + if (tests.testResults.length === 0) { + return []; + } + + const run = tests.testResults[0]; + const snapshots = getTestResultsWithUri(run, uri); + const result: TestMessage[] = []; + + for (const snapshot of snapshots) { + for (const m of snapshot.taskStates[0].messages) { + if ( + m.location && + m.location.range.start.line <= lineNumber && + lineNumber <= m.location.range.end.line + ) { + result.push(m); + } + } + } + + return result; +} + +function getTestResultsWithUri(testRun: TestRunResult, uri: Uri): TestResultSnapshot[] { + const results: TestResultSnapshot[] = []; + + const walk = (r: TestResultSnapshot) => { + for (const c of r.children) { + walk(c); + } + if (r.uri?.toString() === uri.toString()) { + results.push(r); + } + }; + + for (const r of testRun.results) { + walk(r); + } + + return results; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts new file mode 100644 index 00000000000..e04d4beede4 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { spawn } from 'child_process'; +import { existsSync, mkdirSync, renameSync } from 'fs'; +import { readFile, writeFile } from 'fs/promises'; +import { dirname, join } from 'path'; +import * as vscode from 'vscode'; + +interface IGitState { + commitId: string; + tracked: string; + untracked: Record; +} + +interface ITrackedRemediation { + snapshot: vscode.TestResultSnapshot; + failing: IGitState; + passing: IGitState; +} + +const MAX_FAILURES = 10; + +export class FailureTracker { + private readonly disposables: vscode.Disposable[] = []; + private readonly lastFailed = new Map< + string, + { snapshot: vscode.TestResultSnapshot; failing: IGitState } + >(); + + private readonly logFile: string; + private logs?: ITrackedRemediation[]; + + constructor(context: vscode.ExtensionContext, private readonly rootDir: string) { + this.logFile = join(context.globalStorageUri.fsPath, '.build/vscode-test-failures.json'); + mkdirSync(dirname(this.logFile), { recursive: true }); + + const oldLogFile = join(rootDir, '.build/vscode-test-failures.json'); + if (existsSync(oldLogFile)) { + try { + renameSync(oldLogFile, this.logFile); + } catch { + // ignore + } + } + + this.disposables.push( + vscode.commands.registerCommand('selfhost-test-provider.openFailureLog', async () => { + const doc = await vscode.workspace.openTextDocument(this.logFile); + await vscode.window.showTextDocument(doc); + }) + ); + + this.disposables.push( + vscode.tests.onDidChangeTestResults(() => { + const last = vscode.tests.testResults[0]; + if (!last) { + return; + } + + let gitState: Promise | undefined; + const getGitState = () => gitState ?? (gitState = this.captureGitState()); + + const queue = [last.results]; + for (let i = 0; i < queue.length; i++) { + for (const snapshot of queue[i]) { + // only interested in states of leaf tests + if (snapshot.children.length) { + queue.push(snapshot.children); + continue; + } + + const key = `${snapshot.uri}/${snapshot.id}`; + const prev = this.lastFailed.get(key); + if (snapshot.taskStates.some(s => s.state === vscode.TestResultState.Failed)) { + // unset the parent to avoid a circular JSON structure: + getGitState().then(s => + this.lastFailed.set(key, { + snapshot: { ...snapshot, parent: undefined }, + failing: s, + }) + ); + } else if (prev) { + this.lastFailed.delete(key); + getGitState().then(s => this.append({ ...prev, passing: s })); + } + } + } + }) + ); + } + + private async append(log: ITrackedRemediation) { + if (!this.logs) { + try { + this.logs = JSON.parse(await readFile(this.logFile, 'utf-8')); + } catch { + this.logs = []; + } + } + + const logs = this.logs!; + logs.push(log); + if (logs.length > MAX_FAILURES) { + logs.splice(0, logs.length - MAX_FAILURES); + } + + await writeFile(this.logFile, JSON.stringify(logs, undefined, 2)); + } + + private async captureGitState() { + const [commitId, tracked, untracked] = await Promise.all([ + this.exec('git', ['rev-parse', 'HEAD']), + this.exec('git', ['diff', 'HEAD']), + this.exec('git', ['ls-files', '--others', '--exclude-standard']).then(async output => { + const mapping: Record = {}; + await Promise.all( + output + .trim() + .split('\n') + .map(async f => { + mapping[f] = await readFile(join(this.rootDir, f), 'utf-8'); + }) + ); + return mapping; + }), + ]); + return { commitId, tracked, untracked }; + } + + public dispose() { + this.disposables.forEach(d => d.dispose()); + } + + private exec(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: 'pipe', cwd: this.rootDir }); + let output = ''; + child.stdout.setEncoding('utf-8').on('data', b => (output += b)); + child.stderr.setEncoding('utf-8').on('data', b => (output += b)); + child.on('error', reject); + child.on('exit', code => + code === 0 + ? resolve(output) + : reject(new Error(`Failed with error code ${code}\n${output}`)) + ); + }); + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/importGraph.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/importGraph.ts new file mode 100644 index 00000000000..ab3c25720ac --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/importGraph.ts @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { join } from 'path'; +import * as vscode from 'vscode'; +import { bulkhead } from 'cockatiel'; +import { promises as fs } from 'fs'; + +const maxInt32 = 2 ** 31 - 1; + +// limit concurrency to avoid overwhelming the filesystem during discovery +const discoverLimiter = bulkhead(8, Infinity); + +// Max import distance when listing related code to improve relevancy. +const defaultMaxDistance = 3; + +/** + * Maintains a graph of imports in the codebase. This works lazily resolving + * imports and re-parsing files only on request. + * + * This is a rough, file-level graph derived from simple regex matching on + * source files to avoid having to parse the AST of every file in the codebase, + * which is possible but more intensive. (See: all the years of work from the + * TS language server.) + * + * A more advanced implementation could use references from the language server. + */ +export class ImportGraph implements vscode.TestRelatedCodeProvider { + private graph = new Map(); + + constructor( + private readonly root: vscode.Uri, + private readonly discoverWorkspaceTests: () => Thenable, + private readonly getTestNodeForDoc: (uri: vscode.Uri) => vscode.TestItem | undefined, + ) { } + + /** @inheritdoc */ + public async provideRelatedCode(test: vscode.TestItem, token: vscode.CancellationToken): Promise { + // this is kind of a stub for this implementation. Naive following imports + // isn't that useful for finding a test's related code. + const node = await this.discoverOutwards(test.uri, new Set(), defaultMaxDistance, token); + if (!node) { + return []; + } + + const imports = new Set(); + const queue = [{ distance: 0, next: node.imports }]; + while (queue.length) { + const { distance, next } = queue.shift()!; + for (const imp of next) { + if (imports.has(imp.path)) { + continue; + } + + imports.add(imp.path); + if (distance < defaultMaxDistance) { + queue.push({ next: imp.imports, distance: distance + 1 }); + } + } + } + + return [...imports].map(importPath => + new vscode.Location( + vscode.Uri.file(join(this.root.fsPath, 'src', `${importPath}.ts`)), + new vscode.Range(0, 0, maxInt32, 0), + ), + ); + } + + /** @inheritdoc */ + public async provideRelatedTests(document: vscode.TextDocument, _position: vscode.Position, token: vscode.CancellationToken): Promise { + // Expand all known tests to ensure imports of this file are realized. + const rootTests = await this.discoverWorkspaceTests(); + const seen = new Set(); + await Promise.all(rootTests.map(v => v.uri && this.discoverOutwards(v.uri, seen, defaultMaxDistance, token))); + + const node = this.getNode(document.uri); + if (!node) { + return []; + } + + const tests: vscode.TestItem[] = []; + const queue: { next: FileNode; distance: number }[] = [{ next: node, distance: 0 }]; + const visited = new Set(); + let maxDistance = Infinity; + + while (queue.length) { + const { next, distance } = queue.shift()!; + if (visited.has(next)) { + continue; + } + + visited.add(next); + const testForDoc = this.getTestNodeForDoc(next.uri); + if (testForDoc) { + tests.push(testForDoc); + // only look for tests half again as far away as the closest test to keep things relevant + if (!Number.isFinite(maxDistance)) { + maxDistance = distance * 3 / 2; + } + } + + if (distance < maxDistance) { + for (const importedByNode of next.importedBy) { + queue.push({ next: importedByNode, distance: distance + 1 }); + } + } + } + + return tests; + } + + public didChange(uri: vscode.Uri, deleted: boolean) { + const rel = this.uriToImportPath(uri); + const node = rel && this.graph.get(rel); + if (!node) { + return; + } + + if (deleted) { + this.graph.delete(rel); + for (const imp of node.imports) { + imp.importedBy.delete(node); + } + } else { + node.isSynced = false; + } + } + + private getNode(uri: vscode.Uri | undefined): FileNode | undefined { + const rel = this.uriToImportPath(uri); + return rel ? this.graph.get(rel) : undefined; + } + + /** Discover all nodes that import the file */ + private async discoverOutwards(uri: vscode.Uri | undefined, seen: Set, maxDistance: number, token: vscode.CancellationToken): Promise { + const rel = this.uriToImportPath(uri); + if (!rel) { + return undefined; + } + + let node = this.graph.get(rel); + if (!node) { + node = new FileNode(uri!, rel); + this.graph.set(rel, node); + } + + await this.discoverOutwardsInner(node, seen, maxDistance, token); + return node; + } + + private async discoverOutwardsInner(node: FileNode, seen: Set, maxDistance: number, token: vscode.CancellationToken) { + if (seen.has(node.path) || maxDistance === 0) { + return; + } + + seen.add(node.path); + if (node.isSynced === false) { + await this.syncNode(node); + } else if (node.isSynced instanceof Promise) { + await node.isSynced; + } + + if (token.isCancellationRequested) { + return; + } + await Promise.all([...node.imports].map(i => this.discoverOutwardsInner(i, seen, maxDistance - 1, token))); + } + + private async syncNode(node: FileNode) { + node.isSynced = discoverLimiter.execute(async () => { + const doc = vscode.workspace.textDocuments.find(d => d.uri.toString() === node.uri.toString()); + + let text: string; + if (doc) { + text = doc.getText(); + } else { + try { + text = await fs.readFile(node.uri.fsPath, 'utf8'); + } catch { + text = ''; + } + } + + for (const imp of node.imports) { + imp.importedBy.delete(node); + } + node.imports.clear(); + + for (const [, importPath] of text.matchAll(IMPORT_RE)) { + let imp = this.graph.get(importPath); + if (!imp) { + imp = new FileNode(this.importPathToUri(importPath), importPath); + this.graph.set(importPath, imp); + } + + imp.importedBy.add(node); + node.imports.add(imp); + } + + node.isSynced = true; + }); + + await node.isSynced; + } + + private uriToImportPath(uri: vscode.Uri | undefined) { + if (!uri) { + return undefined; + } + + const relativePath = vscode.workspace.asRelativePath(uri).replaceAll('\\', '/'); + if (!relativePath.startsWith('src/vs/') || !relativePath.endsWith('.ts')) { + return undefined; + } + + return relativePath.slice('src/'.length, -'.ts'.length); + } + + private importPathToUri(importPath: string) { + return vscode.Uri.file(join(this.root.fsPath, 'src', `${importPath}.ts`)); + } +} + +const IMPORT_RE = /import .*? from ["'](vs\/[^"']+)/g; + +class FileNode { + public imports = new Set(); + public importedBy = new Set(); + public isSynced: boolean | Promise = false; + + // Path is the *import path* starting with `vs/` + constructor( + public readonly uri: vscode.Uri, + public readonly path: string, + ) { } +} diff --git a/src/vs/base/common/observableValue.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts similarity index 56% rename from src/vs/base/common/observableValue.ts rename to .vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts index 65ac4d6c90e..df6c2e77ed2 100644 --- a/src/vs/base/common/observableValue.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; -//@ts-ignore -import type { IObservable } from 'vs/base/common/observable'; - -/** - * @deprecated Use {@link IObservable} instead. - */ -export interface IObservableValue { - onDidChange: Event; - readonly value: T; -} +export const memoizeLast = (fn: (args: A) => T): ((args: A) => T) => { + let last: { arg: A; result: T } | undefined; + return arg => { + if (last && last.arg === arg) { + return last.result; + } + const result = fn(arg); + last = { arg, result }; + return result; + }; +}; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts new file mode 100644 index 00000000000..8b44c52b72f --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { TestMessage } from 'vscode'; + +export interface TestMessageMetadata { + expectedValue: unknown; + actualValue: unknown; +} + +const cache = new Array<{ id: string; metadata: TestMessageMetadata }>(); + +let id = 0; + +function getId(): string { + return `msg:${id++}:`; +} + +const regexp = /msg:\d+:/; + +export function attachTestMessageMetadata( + message: TestMessage, + metadata: TestMessageMetadata +): void { + const existingMetadata = getTestMessageMetadata(message); + if (existingMetadata) { + Object.assign(existingMetadata, metadata); + return; + } + + const id = getId(); + + if (typeof message.message === 'string') { + message.message = `${message.message}\n${id}`; + } else { + message.message.appendText(`\n${id}`); + } + + cache.push({ id, metadata }); + while (cache.length > 100) { + cache.shift(); + } +} + +export function getTestMessageMetadata(message: TestMessage): TestMessageMetadata | undefined { + let value: string; + if (typeof message.message === 'string') { + value = message.message; + } else { + value = message.message.value; + } + + const result = regexp.exec(value); + if (!result) { + return undefined; + } + + const id = result[0]; + return cache.find(c => c.id === id)?.metadata; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts new file mode 100644 index 00000000000..5b3a624617e --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.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 { promises as fs } from 'fs'; +import * as vscode from 'vscode'; + +export const snapshotComment = '\n\n// Snapshot file: '; + +export const registerSnapshotUpdate = (ctrl: vscode.TestController) => + vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => { + const message: vscode.TestMessage = args.message; + const index = message.expectedOutput?.indexOf(snapshotComment); + if (!message.expectedOutput || !message.actualOutput || !index || index === -1) { + vscode.window.showErrorMessage('Could not find snapshot comment in message'); + return; + } + + const file = message.expectedOutput.slice(index + snapshotComment.length); + await fs.writeFile(file, message.actualOutput); + ctrl.invalidateTestResults(args.test); + }); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts new file mode 100644 index 00000000000..56b26cafda8 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree'; + +const suiteNames = new Set(['suite', 'flakySuite']); + +export const enum Action { + Skip, + Recurse, +} + +export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: VSCodeTest) => { + if (!ts.isCallExpression(node)) { + return Action.Recurse; + } + + let lhs = node.expression; + if (isSkipCall(lhs)) { + return Action.Skip; + } + + if (isPropertyCall(lhs) && lhs.name.text === 'only') { + lhs = lhs.expression; + } + + const name = node.arguments[0]; + const func = node.arguments[1]; + if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) { + return Action.Recurse; + } + + if (!func) { + return Action.Recurse; + } + + const start = src.getLineAndCharacterOfPosition(name.pos); + const end = src.getLineAndCharacterOfPosition(func.end); + const range = new vscode.Range( + new vscode.Position(start.line, start.character), + new vscode.Position(end.line, end.character) + ); + + const cparent = parent instanceof TestConstruct ? parent : undefined; + if (lhs.escapedText === 'test') { + return new TestCase(name.text, range, cparent); + } + + if (suiteNames.has(lhs.escapedText.toString())) { + return new TestSuite(name.text, range, cparent); + } + + return Action.Recurse; +}; + +const isPropertyCall = ( + lhs: ts.LeftHandSideExpression +): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } => + ts.isPropertyAccessExpression(lhs) && + ts.isIdentifier(lhs.expression) && + ts.isIdentifier(lhs.name); + +const isSkipCall = (lhs: ts.LeftHandSideExpression) => + isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip'; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts new file mode 100644 index 00000000000..ca3236ce96a --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Copied from https://github.com/microsoft/vscode-js-debug/blob/1d104b5184736677ab5cc280c70bbd227403850c/src/common/stackTraceParser.ts#L18 + +// Either match lines like +// " at fulfilled (/Users/roblou/code/testapp-node2/out/app.js:5:58)" +// or +// " at /Users/roblou/code/testapp-node2/out/app.js:60:23" +// and replace the path in them +const re1 = /^(\W*at .*\()(.*):(\d+):(\d+)(\))$/; +const re2 = /^(\W*at )(.*):(\d+):(\d+)$/; + +const getLabelRe = /^\W*at (.*) \($/; + +/** + * Parses a textual stack trace. + */ +export class StackTraceParser { + /** Gets whether the stacktrace has any locations in it. */ + public static isStackLike(str: string) { + return re1.test(str) || re2.test(str); + } + constructor(private readonly stack: string) { } + + /** Iterates over segments of text and locations in the stack. */ + *[Symbol.iterator]() { + for (const line of this.stack.split('\n')) { + const match = re1.exec(line) || re2.exec(line); + if (!match) { + yield line + '\n'; + continue; + } + + const [, prefix, url, lineNo, columnNo, suffix] = match; + if (prefix) { + yield prefix; + } + + yield new StackTraceLocation(getLabelRe.exec(prefix)?.[1], url, Number(lineNo), Number(columnNo)); + + if (suffix) { + yield suffix; + } + + yield '\n'; + } + } +} + +export class StackTraceLocation { + constructor( + public readonly label: string | undefined, + public readonly path: string, + public readonly lineBase1: number, + public readonly columnBase1: number, + ) { } +} \ No newline at end of file diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts new file mode 100644 index 00000000000..bb5bc5f28dd --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.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. + *--------------------------------------------------------------------------------------------*/ + +// DO NOT EDIT DIRECTLY: copied from src/vs/base/node/nodeStreams.ts + +import { Transform } from 'stream'; + +/** + * A Transform stream that splits the input on the "splitter" substring. + * The resulting chunks will contain (and trail with) the splitter match. + * The last chunk when the stream ends will be emitted even if a splitter + * is not encountered. + */ +export class StreamSplitter extends Transform { + private buffer: Buffer | undefined; + private readonly splitter: number; + private readonly spitterLen: number; + + constructor(splitter: string | number | Buffer) { + super(); + if (typeof splitter === 'number') { + this.splitter = splitter; + this.spitterLen = 1; + } else { + throw new Error('not implemented here'); + } + } + + override _transform( + chunk: Buffer, + _encoding: string, + callback: (error?: Error | null, data?: any) => void + ): void { + if (!this.buffer) { + this.buffer = chunk; + } else { + this.buffer = Buffer.concat([this.buffer, chunk]); + } + + let offset = 0; + while (offset < this.buffer.length) { + const index = this.buffer.indexOf(this.splitter, offset); + if (index === -1) { + break; + } + + this.push(this.buffer.slice(offset, index + this.spitterLen)); + offset = index + this.spitterLen; + } + + this.buffer = offset === this.buffer.length ? undefined : this.buffer.slice(offset); + callback(); + } + + override _flush(callback: (error?: Error | null, data?: any) => void): void { + if (this.buffer) { + this.push(this.buffer); + } + + callback(); + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts new file mode 100644 index 00000000000..f8e70b4d9cd --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -0,0 +1,660 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + decodedMappings, + GREATEST_LOWER_BOUND, + LEAST_UPPER_BOUND, + originalPositionFor, + TraceMap, +} from '@jridgewell/trace-mapping'; +import * as styles from 'ansi-styles'; +import { ChildProcessWithoutNullStreams } from 'child_process'; +import * as vscode from 'vscode'; +import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider'; +import { attachTestMessageMetadata } from './metadata'; +import { snapshotComment } from './snapshot'; +import { StackTraceLocation, StackTraceParser } from './stackTraceParser'; +import { StreamSplitter } from './streamSplitter'; +import { getContentFromFilesystem } from './testTree'; +import { IScriptCoverage } from './v8CoverageWrangling'; + +export const enum MochaEvent { + Start = 'start', + TestStart = 'testStart', + Pass = 'pass', + Fail = 'fail', + End = 'end', + + // custom events: + CoverageInit = 'coverageInit', + CoverageIncrement = 'coverageIncrement', +} + +export interface IStartEvent { + total: number; +} + +export interface ITestStartEvent { + title: string; + fullTitle: string; + file: string; + currentRetry: number; + speed: string; +} + +export interface IPassEvent extends ITestStartEvent { + duration: number; +} + +export interface IFailEvent extends IPassEvent { + err: string; + stack: string | null; + expected?: string; + actual?: string; + expectedJSON?: unknown; + actualJSON?: unknown; + snapshotPath?: string; +} + +export interface IEndEvent { + suites: number; + tests: number; + passes: number; + pending: number; + failures: number; + start: string /* ISO date */; + end: string /* ISO date */; +} + +export interface ITestCoverageCoverage { + file: string; + fullTitle: string; + coverage: { result: IScriptCoverage[] }; +} + +export type MochaEventTuple = + | [MochaEvent.Start, IStartEvent] + | [MochaEvent.TestStart, ITestStartEvent] + | [MochaEvent.Pass, IPassEvent] + | [MochaEvent.Fail, IFailEvent] + | [MochaEvent.End, IEndEvent] + | [MochaEvent.CoverageInit, { result: IScriptCoverage[] }] + | [MochaEvent.CoverageIncrement, ITestCoverageCoverage]; + +const LF = '\n'.charCodeAt(0); + +export class TestOutputScanner implements vscode.Disposable { + protected mochaEventEmitter = new vscode.EventEmitter(); + protected outputEventEmitter = new vscode.EventEmitter(); + protected onExitEmitter = new vscode.EventEmitter(); + + /** + * Fired when a mocha event comes in. + */ + public readonly onMochaEvent = this.mochaEventEmitter.event; + + /** + * Fired when other output from the process comes in. + */ + public readonly onOtherOutput = this.outputEventEmitter.event; + + /** + * Fired when the process encounters an error, or exits. + */ + public readonly onRunnerExit = this.onExitEmitter.event; + + constructor(private readonly process: ChildProcessWithoutNullStreams, private args?: string[]) { + process.stdout.pipe(new StreamSplitter(LF)).on('data', this.processData); + process.stderr.pipe(new StreamSplitter(LF)).on('data', this.processData); + process.on('error', e => this.onExitEmitter.fire(e.message)); + process.on('exit', code => + this.onExitEmitter.fire(code ? `Test process exited with code ${code}` : undefined) + ); + } + + /** + * @override + */ + public dispose() { + try { + this.process.kill(); + } catch { + // ignored + } + } + + protected readonly processData = (data: string | Buffer) => { + if (this.args) { + this.outputEventEmitter.fire(`./scripts/test ${this.args.join(' ')}`); + this.args = undefined; + } + + data = data.toString(); + + try { + const parsed = JSON.parse(data.trim()) as unknown; + if (parsed instanceof Array && parsed.length === 2 && typeof parsed[0] === 'string') { + this.mochaEventEmitter.fire(parsed as MochaEventTuple); + } else { + this.outputEventEmitter.fire(data); + } + } catch { + this.outputEventEmitter.fire(data); + } + }; +} + +type QueuedOutput = string | [string, vscode.Location | undefined, vscode.TestItem | undefined]; + +export async function scanTestOutput( + tests: Map, + task: vscode.TestRun, + scanner: TestOutputScanner, + coverageDir: string | undefined, + cancellation: vscode.CancellationToken +): Promise { + const exitBlockers: Set> = new Set(); + const skippedTests = new Set(tests.values()); + const store = new SourceMapStore(); + let outputQueue = Promise.resolve(); + const enqueueOutput = (fn: QueuedOutput | (() => Promise)) => { + exitBlockers.delete(outputQueue); + outputQueue = outputQueue.finally(async () => { + const r = typeof fn === 'function' ? await fn() : fn; + typeof r === 'string' ? task.appendOutput(r) : task.appendOutput(...r); + }); + exitBlockers.add(outputQueue); + return outputQueue; + }; + const enqueueExitBlocker = (prom: Promise): Promise => { + exitBlockers.add(prom); + prom.finally(() => exitBlockers.delete(prom)); + return prom; + }; + + let perTestCoverage: PerTestCoverageTracker | undefined; + let lastTest: vscode.TestItem | undefined; + let ranAnyTest = false; + + try { + if (cancellation.isCancellationRequested) { + return; + } + + await new Promise(resolve => { + cancellation.onCancellationRequested(() => { + resolve(); + }); + + let currentTest: vscode.TestItem | undefined; + + scanner.onRunnerExit(err => { + if (err) { + enqueueOutput(err + crlf); + } + resolve(); + }); + + scanner.onOtherOutput(str => { + const match = spdlogRe.exec(str); + if (!match) { + enqueueOutput(str + crlf); + return; + } + + const logLocation = store.getSourceLocation(match[2], Number(match[3]) - 1); + const logContents = replaceAllLocations(store, match[1]); + const test = currentTest; + + enqueueOutput(() => + Promise.all([logLocation, logContents]).then(([location, contents]) => [ + contents + crlf, + location, + test, + ]) + ); + }); + + scanner.onMochaEvent(evt => { + switch (evt[0]) { + case MochaEvent.Start: + break; // no-op + case MochaEvent.TestStart: + currentTest = tests.get(evt[1].fullTitle); + if (!currentTest) { + console.warn(`Could not find test ${evt[1].fullTitle}`); + return; + } + skippedTests.delete(currentTest); + task.started(currentTest); + ranAnyTest = true; + break; + case MochaEvent.Pass: + { + const title = evt[1].fullTitle; + const tcase = tests.get(title); + enqueueOutput(` ${styles.green.open}√${styles.green.close} ${title}\r\n`); + if (tcase) { + lastTest = tcase; + task.passed(tcase, evt[1].duration); + } + } + break; + case MochaEvent.Fail: + { + const { + err, + stack, + duration, + expected, + expectedJSON, + actual, + actualJSON, + snapshotPath, + fullTitle: id, + } = evt[1]; + let tcase = tests.get(id); + // report failures on hook to the last-seen test, or first test if none run yet + if (!tcase && (id.includes('hook for') || id.includes('hook in'))) { + tcase = lastTest ?? tests.values().next().value; + } + + enqueueOutput(`${styles.red.open} x ${id}${styles.red.close}\r\n`); + const rawErr = stack || err; + const locationsReplaced = replaceAllLocations(store, forceCRLF(rawErr)); + if (rawErr) { + enqueueOutput(async () => [await locationsReplaced, undefined, tcase]); + } + + if (!tcase) { + return; + } + + const hasDiff = + actual !== undefined && + expected !== undefined && + (expected !== '[undefined]' || actual !== '[undefined]'); + const testFirstLine = + tcase.range && + new vscode.Location( + tcase.uri!, + new vscode.Range( + tcase.range.start, + new vscode.Position(tcase.range.start.line, 100) + ) + ); + + enqueueExitBlocker( + (async () => { + const stackInfo = await deriveStackLocations(store, rawErr, tcase!); + let message: vscode.TestMessage2; + + if (hasDiff) { + message = new vscode.TestMessage(tryMakeMarkdown(err)); + message.actualOutput = outputToString(actual); + message.expectedOutput = outputToString(expected); + if (snapshotPath) { + message.contextValue = 'isSelfhostSnapshotMessage'; + message.expectedOutput += snapshotComment + snapshotPath; + } + + attachTestMessageMetadata(message, { + expectedValue: expectedJSON, + actualValue: actualJSON, + }); + } else { + message = new vscode.TestMessage( + stack ? await sourcemapStack(store, stack) : await locationsReplaced + ); + } + + message.location = stackInfo.primary ?? testFirstLine; + message.stackTrace = stackInfo.stack; + task.failed(tcase!, message, duration); + })() + ); + } + break; + case MochaEvent.End: + // no-op, we wait until the process exits to ensure coverage is written out + break; + case MochaEvent.CoverageInit: + perTestCoverage ??= new PerTestCoverageTracker(store); + for (const result of evt[1].result) { + perTestCoverage.add(result); + } + break; + case MochaEvent.CoverageIncrement: { + const { fullTitle, coverage } = evt[1]; + const tcase = tests.get(fullTitle); + if (tcase) { + perTestCoverage ??= new PerTestCoverageTracker(store); + for (const result of coverage.result) { + perTestCoverage.add(result, tcase); + } + } + break; + } + } + }); + }); + + if (perTestCoverage) { + enqueueExitBlocker(perTestCoverage.report(task)); + } + + await Promise.all([...exitBlockers]); + + if (coverageDir) { + try { + await istanbulCoverageContext.apply(task, coverageDir, { + mapFileUri: uri => store.getSourceFile(uri.toString()), + mapLocation: (uri, position) => + store.getSourceLocation(uri.toString(), position.line, position.character), + }); + } catch (e) { + const msg = `Error loading coverage:\n\n${e}\n`; + task.appendOutput(msg.replace(/\n/g, crlf)); + } + } + + // no tests? Possible crash, show output: + if (!ranAnyTest) { + await vscode.commands.executeCommand('testing.showMostRecentOutput'); + } + } catch (e) { + task.appendOutput((e as Error).stack || (e as Error).message); + } finally { + scanner.dispose(); + for (const test of skippedTests) { + task.skipped(test); + } + task.end(); + } +} + +const spdlogRe = /"(.+)", source: (file:\/\/\/.*?)+ \(([0-9]+)\)/; +const crlf = '\r\n'; + +const forceCRLF = (str: string) => str.replace(/(? { + locationRe.lastIndex = 0; + + const replacements = await Promise.all( + [...str.matchAll(locationRe)].map(async match => { + const location = await deriveSourceLocation(store, match); + if (!location) { + return; + } + return { + from: match[0], + to: location?.uri.with({ + fragment: `L${location.range.start.line + 1}:${location.range.start.character + 1}`, + }), + }; + }) + ); + + for (const replacement of replacements) { + if (replacement) { + str = str.replace(replacement.from, replacement.to.toString(true)); + } + } + + return str; +}; + +const outputToString = (output: unknown) => + typeof output === 'object' ? JSON.stringify(output, null, 2) : String(output); + +const tryMakeMarkdown = (message: string) => { + const lines = message.split('\n'); + const start = lines.findIndex(l => l.includes('+ actual')); + if (start === -1) { + return message; + } + + lines.splice(start, 1, '```diff'); + lines.push('```'); + return new vscode.MarkdownString(lines.join('\n')); +}; + +const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; +const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; + +export const enum SearchStrategy { + FirstBefore = -1, + FirstAfter = 1, +} + +export type SourceLocationMapper = (line: number, col: number, strategy: SearchStrategy) => vscode.Location | undefined; + +export class SourceMapStore { + private readonly cache = new Map>(); + + async getSourceLocationMapper(fileUri: string): Promise { + const sourceMap = await this.loadSourceMap(fileUri); + return (line, col, strategy) => { + if (!sourceMap) { + return undefined; + } + + // 1. Look for the ideal position on this line if it exists + const idealPosition = originalPositionFor(sourceMap, { column: col, line: line + 1, bias: SearchStrategy.FirstAfter ? GREATEST_LOWER_BOUND : LEAST_UPPER_BOUND }); + if (idealPosition.line !== null && idealPosition.column !== null && idealPosition.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, idealPosition.source), + new vscode.Position(idealPosition.line - 1, idealPosition.column) + ); + } + + // Otherwise get the first/last valid mapping on another line. + const decoded = decodedMappings(sourceMap); + const enum MapField { + COLUMN = 0, + SOURCES_INDEX = 1, + SOURCE_LINE = 2, + SOURCE_COLUMN = 3, + } + + do { + line += strategy; + const segments = decoded[line]; + if (!segments?.length) { + continue; + } + + const index = strategy === SearchStrategy.FirstBefore + ? findLastIndex(segments, s => s.length !== 1) + : segments.findIndex(s => s.length !== 1); + const segment = segments[index]; + + if (!segment || segment.length === 1) { + continue; + } + + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, sourceMap.sources[segment[MapField.SOURCES_INDEX]]!), + new vscode.Position(segment[MapField.SOURCE_LINE] - 1, segment[MapField.SOURCE_COLUMN]) + ); + } while (strategy === SearchStrategy.FirstBefore ? line > 0 : line < decoded.length); + + return undefined; + }; + } + + /** Gets an original location from a base 0 line and column */ + async getSourceLocation(fileUri: string, line: number, col = 0) { + const sourceMap = await this.loadSourceMap(fileUri); + if (!sourceMap) { + return undefined; + } + + let smLine = line + 1; + + // if the range is after the end of mappings, adjust it to the last mapped line + const decoded = decodedMappings(sourceMap); + if (decoded.length <= line) { + smLine = decoded.length; // base 1, no -1 needed + col = Number.MAX_SAFE_INTEGER; + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias }); + if (position.line !== null && position.column !== null && position.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, position.source), + new vscode.Position(position.line - 1, position.column) + ); + } + } + + return undefined; + } + + async getSourceFile(compiledUri: string) { + const sourceMap = await this.loadSourceMap(compiledUri); + if (!sourceMap) { + return undefined; + } + + if (sourceMap.sources[0]) { + return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]); + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias }); + if (position.source !== null) { + return this.completeSourceMapUrl(sourceMap, position.source); + } + } + + return undefined; + } + + private completeSourceMapUrl(sm: TraceMap, source: string) { + if (sm.sourceRoot) { + try { + return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString()); + } catch { + // ignored + } + } + + return vscode.Uri.parse(source); + } + + private loadSourceMap(fileUri: string) { + const existing = this.cache.get(fileUri); + if (existing) { + return existing; + } + + const promise = (async () => { + try { + const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri)); + const sourcemapMatch = inlineSourcemapRe.exec(contents); + if (!sourcemapMatch) { + return; + } + + const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString(); + return new TraceMap(decoded, fileUri); + } catch (e) { + console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`); + return; + } + })(); + + this.cache.set(fileUri, promise); + return promise; + } +} + +const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g; + +async function replaceAllLocations(store: SourceMapStore, str: string) { + const output: (string | Promise)[] = []; + let lastIndex = 0; + + for (const match of str.matchAll(locationRe)) { + const locationPromise = deriveSourceLocation(store, match); + const startIndex = match.index || 0; + const endIndex = startIndex + match[0].length; + + if (startIndex > lastIndex) { + output.push(str.substring(lastIndex, startIndex)); + } + + output.push( + locationPromise.then(location => + location + ? `${location.uri}:${location.range.start.line + 1}:${location.range.start.character + 1}` + : match[0] + ) + ); + + lastIndex = endIndex; + } + + // Preserve the remaining string after the last match + if (lastIndex < str.length) { + output.push(str.substring(lastIndex)); + } + + const values = await Promise.all(output); + return values.join(''); +} + +async function deriveStackLocations( + store: SourceMapStore, + stack: string, + tcase: vscode.TestItem +) { + locationRe.lastIndex = 0; + + const locationsRaw = [...new StackTraceParser(stack)].filter(t => t instanceof StackTraceLocation); + const locationsMapped = await Promise.all(locationsRaw.map(async location => { + const mapped = location.path.startsWith('file:') ? await store.getSourceLocation(location.path, location.lineBase1 - 1, location.columnBase1 - 1) : undefined; + const stack = new vscode.TestMessageStackFrame(location.label || '', mapped?.uri, mapped?.range.start || new vscode.Position(location.lineBase1 - 1, location.columnBase1 - 1)); + return { location: mapped, stack }; + })); + + let best: undefined | { location: vscode.Location; score: number }; + for (const { location } of locationsMapped) { + if (!location) { + continue; + } + let score = 0; + if (tcase.uri && tcase.uri.toString() === location.uri.toString()) { + score = 1; + if (tcase.range && tcase.range.contains(location?.range)) { + score = 2; + } + } + if (!best || score > best.score) { + best = { location, score }; + } + } + + return { stack: locationsMapped.map(s => s.stack), primary: best?.location }; +} + +async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) { + const [, fileUri, line, col] = parts; + return store.getSourceLocation(fileUri, Number(line) - 1, Number(col)); +} + +function findLastIndex(arr: T[], predicate: (value: T) => boolean) { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return i; + } + } + + return -1; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts new file mode 100644 index 00000000000..7a54c5c0d32 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { join, relative } from 'path'; +import * as ts from 'typescript'; +import { TextDecoder } from 'util'; +import * as vscode from 'vscode'; +import { Action, extractTestFromNode } from './sourceUtils'; + +const textDecoder = new TextDecoder('utf-8'); +const diagnosticCollection = vscode.languages.createDiagnosticCollection('selfhostTestProvider'); + +type ContentGetter = (uri: vscode.Uri) => Promise; + +export const itemData = new WeakMap(); + +export const clearFileDiagnostics = (uri: vscode.Uri) => diagnosticCollection.delete(uri); + +/** + * Tries to guess which workspace folder VS Code is in. + */ +export const guessWorkspaceFolder = async () => { + if (!vscode.workspace.workspaceFolders) { + return undefined; + } + + if (vscode.workspace.workspaceFolders.length < 2) { + return vscode.workspace.workspaceFolders[0]; + } + + for (const folder of vscode.workspace.workspaceFolders) { + try { + await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); + return folder; + } catch { + // ignored + } + } + + return undefined; +}; + +export const getContentFromFilesystem: ContentGetter = async uri => { + try { + const rawContent = await vscode.workspace.fs.readFile(uri); + return textDecoder.decode(rawContent); + } catch (e) { + console.warn(`Error providing tests for ${uri.fsPath}`, e); + return ''; + } +}; + +export class TestFile { + public hasBeenRead = false; + + constructor( + public readonly uri: vscode.Uri, + public readonly workspaceFolder: vscode.WorkspaceFolder + ) {} + + public getId() { + return this.uri.toString().toLowerCase(); + } + + public getLabel() { + return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath); + } + + public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { + try { + const content = await getContentFromFilesystem(item.uri!); + item.error = undefined; + this.updateFromContents(controller, content, item); + } catch (e) { + item.error = (e as Error).stack; + } + } + + /** + * Refreshes all tests in this file, `sourceReader` provided by the root. + */ + public updateFromContents( + controller: vscode.TestController, + content: string, + file: vscode.TestItem + ) { + try { + const diagnostics: vscode.Diagnostic[] = []; + const ast = ts.createSourceFile( + this.uri.path.split('/').pop()!, + content, + ts.ScriptTarget.ESNext, + false, + ts.ScriptKind.TS + ); + + const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [ + { item: file, children: [] }, + ]; + const traverse = (node: ts.Node) => { + const parent = parents[parents.length - 1]; + const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!); + if (childData === Action.Skip) { + return; + } + + if (childData === Action.Recurse) { + ts.forEachChild(node, traverse); + return; + } + + const id = `${file.uri}/${childData.fullName}`.toLowerCase(); + + // Skip duplicated tests. They won't run correctly with the way + // mocha reports them, and will error if we try to insert them. + const existing = parent.children.find(c => c.id === id); + if (existing) { + const diagnostic = new vscode.Diagnostic( + childData.range, + 'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.', + vscode.DiagnosticSeverity.Warning + ); + + diagnostic.relatedInformation = [ + new vscode.DiagnosticRelatedInformation( + new vscode.Location(existing.uri!, existing.range!), + 'First declared here' + ), + ]; + + diagnostics.push(diagnostic); + return; + } + + const item = controller.createTestItem(id, childData.name, file.uri); + itemData.set(item, childData); + item.range = childData.range; + parent.children.push(item); + + if (childData instanceof TestSuite) { + parents.push({ item: item, children: [] }); + ts.forEachChild(node, traverse); + item.children.replace(parents.pop()!.children); + } + }; + + ts.forEachChild(ast, traverse); + file.error = undefined; + file.children.replace(parents[0].children); + diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined); + this.hasBeenRead = true; + } catch (e) { + file.error = String((e as Error).stack || (e as Error).message); + } + } +} + +export abstract class TestConstruct { + public fullName: string; + + constructor( + public readonly name: string, + public readonly range: vscode.Range, + parent?: TestConstruct + ) { + this.fullName = parent ? `${parent.fullName} ${name}` : name; + } +} + +export class TestSuite extends TestConstruct {} + +export class TestCase extends TestConstruct {} + +export type VSCodeTest = TestFile | TestSuite | TestCase; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts new file mode 100644 index 00000000000..c2564ca61c3 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { RangeCoverageTracker } from './v8CoverageWrangling'; + +suite('v8CoverageWrangling', () => { + suite('RangeCoverageTracker', () => { + test('covers new range', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); + }); + + test('non overlapping ranges', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.cover(15, 20); + rt.cover(12, 13); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 10, covered: true }, + { start: 12, end: 13, covered: true }, + { start: 15, end: 20, covered: true }, + ] + ); + }); + + test('covers exact', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); + }); + + test('overlap at start', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(2, 7); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 7, covered: true }, + { start: 7, end: 10, covered: false }, + ] + ); + }); + + test('overlap at end', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 7); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + ] + ); + }); + + test('inner contained', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 12); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + { start: 10, end: 12, covered: false }, + ] + ); + }); + + test('outer contained', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(7, 9); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 7, covered: false }, + { start: 7, end: 9, covered: true }, + { start: 9, end: 10, covered: false }, + ] + ); + }); + + test('boundary touching', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(10, 15); + rt.uncovered(15, 20); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 10, covered: false }, + { start: 10, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + ] + ); + }); + + suite('initializeBlock', () => { + test('simple tree', () => { + const rt = RangeCoverageTracker.initializeBlocks([ + { + functionName: 'outer', + isBlockCoverage: true, + ranges: [ + { count: 1, startOffset: 5, endOffset: 30 }, + { count: 1, startOffset: 8, endOffset: 10 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ], + }, + ]); + + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + { start: 20, end: 30, covered: true }, + ] + ); + }); + + test('separate branches', () => { + const rt = RangeCoverageTracker.initializeBlocks([ + { + functionName: 'outer', + isBlockCoverage: true, + ranges: [ + { count: 1, startOffset: 5, endOffset: 8 }, + { count: 1, startOffset: 10, endOffset: 12 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ], + }, + ]); + + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 8, covered: true }, + { start: 10, end: 12, covered: true }, + { start: 15, end: 20, covered: false }, + ] + ); + }); + }); + }); +}); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts new file mode 100644 index 00000000000..ede638430ba --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface ICoverageRange { + start: number; + end: number; + covered: boolean; +} + +export interface IV8FunctionCoverage { + functionName: string; + isBlockCoverage: boolean; + ranges: IV8CoverageRange[]; +} + +export interface IV8CoverageRange { + startOffset: number; + endOffset: number; + count: number; +} + +/** V8 Script coverage data */ +export interface IScriptCoverage { + scriptId: string; + url: string; + // Script source added by the runner the first time the script is emitted. + source?: string; + functions: IV8FunctionCoverage[]; +} + +export class RangeCoverageTracker implements Iterable { + /** + * A noncontiguous, non-overlapping, ordered set of ranges and whether + * that range has been covered. + */ + private ranges: readonly ICoverageRange[] = []; + + /** + * Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true. + */ + public static initializeBlocks(fns: IV8FunctionCoverage[]) { + const rt = new RangeCoverageTracker(); + + let start = 0; + const stack: IV8CoverageRange[] = []; + + // note: comes pre-sorted from V8 + for (const { ranges } of fns) { + for (const range of ranges) { + while (stack.length && stack[stack.length - 1].endOffset < range.startOffset) { + const last = stack.pop()!; + rt.setCovered(start, last.endOffset, last.count > 0); + start = last.endOffset; + } + + if (range.startOffset > start && stack.length) { + rt.setCovered(start, range.startOffset, !!stack[stack.length - 1].count); + } + + start = range.startOffset; + stack.push(range); + } + } + + while (stack.length) { + const last = stack.pop()!; + rt.setCovered(start, last.endOffset, last.count > 0); + start = last.endOffset; + } + + return rt; + } + + /** Makes a copy of the range tracker. */ + public clone() { + const rt = new RangeCoverageTracker(); + rt.ranges = this.ranges.slice(); + return rt; + } + + /** Marks a range covered */ + public cover(start: number, end: number) { + this.setCovered(start, end, true); + } + + /** Marks a range as uncovered */ + public uncovered(start: number, end: number) { + this.setCovered(start, end, false); + } + + /** Iterates over coverage ranges */ + [Symbol.iterator]() { + return this.ranges[Symbol.iterator](); + } + + /** + * Marks the given character range as being covered or uncovered. + * + * todo@connor4312: this is a hot path is could probably be optimized to + * avoid rebuilding the array. Maybe with a nice tree structure? + */ + public setCovered(start: number, end: number, covered: boolean) { + const newRanges: ICoverageRange[] = []; + let i = 0; + for (; i < this.ranges.length && this.ranges[i].end <= start; i++) { + newRanges.push(this.ranges[i]); + } + + const push = (range: ICoverageRange) => { + const last = newRanges.length && newRanges[newRanges.length - 1]; + if (last && last.end === range.start && last.covered === range.covered) { + last.end = range.end; + } else { + newRanges.push(range); + } + }; + + push({ start, end, covered }); + + for (; i < this.ranges.length; i++) { + const range = this.ranges[i]; + const last = newRanges[newRanges.length - 1]; + + if (range.start === last.start && range.end === last.end) { + // ranges are equal: + last.covered ||= range.covered; + } else if (range.end < last.start || range.start > last.end) { + // ranges don't overlap + push(range); + } else if (range.start < last.start && range.end > last.end) { + // range contains last: + newRanges.pop(); + push({ start: range.start, end: last.start, covered: range.covered }); + push({ start: last.start, end: last.end, covered: range.covered || last.covered }); + push({ start: last.end, end: range.end, covered: range.covered }); + } else if (range.start >= last.start && range.end <= last.end) { + // last contains range: + newRanges.pop(); + push({ start: last.start, end: range.start, covered: last.covered }); + push({ start: range.start, end: range.end, covered: range.covered || last.covered }); + push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start < last.start && range.end <= last.end) { + // range overlaps start of last: + newRanges.pop(); + push({ start: range.start, end: last.start, covered: range.covered }); + push({ start: last.start, end: range.end, covered: range.covered || last.covered }); + push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start >= last.start && range.end > last.end) { + // range overlaps end of last: + newRanges.pop(); + push({ start: last.start, end: range.start, covered: last.covered }); + push({ start: range.start, end: last.end, covered: range.covered || last.covered }); + push({ start: last.end, end: range.end, covered: range.covered }); + } else { + throw new Error('unreachable'); + } + } + + this.ranges = newRanges; + } +} + +export class OffsetToPosition { + /** Line numbers to byte offsets. */ + public readonly lines: number[] = []; + + public readonly totalLength: number; + + constructor(source: string) { + this.lines.push(0); + for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) { + this.lines.push(i + 1); + } + this.totalLength = source.length; + } + + public getLineLength(lineNumber: number): number { + return ( + (lineNumber < this.lines.length - 1 ? this.lines[lineNumber + 1] - 1 : this.totalLength) - + this.lines[lineNumber] + ); + } + + /** + * Gets the line the offset appears on. + */ + public getLineOfOffset(offset: number): number { + let low = 0; + let high = this.lines.length; + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (this.lines[mid] > offset) { + high = mid; + } else { + low = mid + 1; + } + } + + return low - 1; + } + + /** + * Converts from a file offset to a base 0 line/column . + */ + public toLineColumn(offset: number): { line: number; column: number } { + const line = this.getLineOfOffset(offset); + return { line: line, column: offset - this.lines[line] }; + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts new file mode 100644 index 00000000000..954b847f4a8 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -0,0 +1,329 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { spawn } from 'child_process'; +import { promises as fs } from 'fs'; +import { AddressInfo, createServer } from 'net'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { TestOutputScanner } from './testOutputScanner'; +import { TestCase, TestFile, TestSuite, itemData } from './testTree'; + +/** + * From MDN + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping + */ +const escapeRe = (s: string) => s.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); + +const TEST_ELECTRON_SCRIPT_PATH = 'test/unit/electron/index.js'; +const TEST_BROWSER_SCRIPT_PATH = 'test/unit/browser/index.js'; + +const ATTACH_CONFIG_NAME = 'Attach to VS Code'; +const DEBUG_TYPE = 'pwa-chrome'; + +export abstract class VSCodeTestRunner { + constructor(protected readonly repoLocation: vscode.WorkspaceFolder) { } + + public async run(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { + const args = this.prepareArguments(baseArgs, filter); + const cp = spawn(await this.binaryPath(), args, { + cwd: this.repoLocation.uri.fsPath, + stdio: 'pipe', + env: this.getEnvironment(), + }); + + return new TestOutputScanner(cp, args); + } + + public async debug(testRun: vscode.TestRun, baseArgs: ReadonlyArray, filter?: ReadonlyArray) { + const port = await this.findOpenPort(); + const baseConfiguration = vscode.workspace + .getConfiguration('launch', this.repoLocation) + .get('configurations', []) + .find(c => c.name === ATTACH_CONFIG_NAME); + + if (!baseConfiguration) { + throw new Error(`Could not find launch configuration ${ATTACH_CONFIG_NAME}`); + } + + const server = this.createWaitServer(); + const args = [ + ...this.prepareArguments(baseArgs, filter), + `--remote-debugging-port=${port}`, + // for breakpoint freeze: https://github.com/microsoft/vscode/issues/122225#issuecomment-885377304 + '--js-flags="--regexp_interpret_all"', + // for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910 + '--disable-features=CalculateNativeWinOcclusion', + '--timeout=0', + `--waitServer=${server.port}`, + ]; + + const cp = spawn(await this.binaryPath(), args, { + cwd: this.repoLocation.uri.fsPath, + stdio: 'pipe', + env: this.getEnvironment(), + }); + + // Register a descriptor factory that signals the server when any + // breakpoint set requests on the debugee have been completed. + const factory = vscode.debug.registerDebugAdapterTrackerFactory(DEBUG_TYPE, { + createDebugAdapterTracker(session) { + if (!session.parentSession || session.parentSession !== rootSession) { + return; + } + + let initRequestId: number | undefined; + + return { + onDidSendMessage(message) { + if (message.type === 'response' && message.request_seq === initRequestId) { + server.ready(); + } + }, + onWillReceiveMessage(message) { + if (initRequestId !== undefined) { + return; + } + + if (message.command === 'launch' || message.command === 'attach') { + initRequestId = message.seq; + } + }, + }; + }, + }); + + vscode.debug.startDebugging(this.repoLocation, { ...baseConfiguration, port }, { testRun }); + + let exited = false; + let rootSession: vscode.DebugSession | undefined; + cp.once('exit', () => { + exited = true; + server.dispose(); + listener.dispose(); + factory.dispose(); + + if (rootSession) { + vscode.debug.stopDebugging(rootSession); + } + }); + + const listener = vscode.debug.onDidStartDebugSession(s => { + if (s.name === ATTACH_CONFIG_NAME && !rootSession) { + if (exited) { + vscode.debug.stopDebugging(rootSession); + } else { + rootSession = s; + } + } + }); + + return new TestOutputScanner(cp, args); + } + + private findOpenPort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, () => { + const address = server.address() as AddressInfo; + const port = address.port; + server.close(() => { + resolve(port); + }); + }); + server.on('error', (error: Error) => { + reject(error); + }); + }); + } + + protected getEnvironment(): NodeJS.ProcessEnv { + return { + ...process.env, + ELECTRON_RUN_AS_NODE: undefined, + ELECTRON_ENABLE_LOGGING: '1', + }; + } + + private prepareArguments( + baseArgs: ReadonlyArray, + filter?: ReadonlyArray + ) { + const args = [...this.getDefaultArgs(), ...baseArgs, '--reporter', 'full-json-stream']; + if (!filter) { + return args; + } + + const grepRe: string[] = []; + const runPaths = new Set(); + const addTestFileRunPath = (data: TestFile) => + runPaths.add( + path.relative(data.workspaceFolder.uri.fsPath, data.uri.fsPath).replace(/\\/g, '/') + ); + + const itemDatas = filter.map(f => itemData.get(f)); + /** If true, we have to be careful with greps, as a grep for one test file affects the run of the other test file. */ + const hasBothTestCaseOrTestSuiteAndTestFileFilters = + itemDatas.some(d => (d instanceof TestCase) || (d instanceof TestSuite)) && + itemDatas.some(d => d instanceof TestFile); + + function addTestCaseOrSuite(data: TestCase | TestSuite, test: vscode.TestItem): void { + grepRe.push(escapeRe(data.fullName) + (data instanceof TestCase ? '$' : ' ')); + for (let p = test.parent; p; p = p.parent) { + const parentData = itemData.get(p); + if (parentData instanceof TestFile) { + addTestFileRunPath(parentData); + } + } + } + + for (const test of filter) { + const data = itemData.get(test); + if (data instanceof TestCase || data instanceof TestSuite) { + addTestCaseOrSuite(data, test); + } else if (data instanceof TestFile) { + if (!hasBothTestCaseOrTestSuiteAndTestFileFilters) { + addTestFileRunPath(data); + } else { + // We add all the items individually so they get their own grep expressions. + for (const [_id, nestedTest] of test.children) { + const childData = itemData.get(nestedTest); + if (childData instanceof TestCase || childData instanceof TestSuite) { + addTestCaseOrSuite(childData, nestedTest); + } else { + console.error('Unexpected test item in test file', nestedTest.id, nestedTest.label); + } + } + } + } + } + + if (grepRe.length) { + args.push('--grep', `/^(${grepRe.join('|')})/`); + } + + if (runPaths.size) { + args.push(...[...runPaths].flatMap(p => ['--run', p])); + } + + return args; + } + + protected abstract getDefaultArgs(): string[]; + + protected abstract binaryPath(): Promise; + + protected async readProductJson() { + const projectJson = await fs.readFile( + path.join(this.repoLocation.uri.fsPath, 'product.json'), + 'utf-8' + ); + try { + return JSON.parse(projectJson); + } catch (e) { + throw new Error(`Error parsing product.json: ${(e as Error).message}`); + } + } + + private createWaitServer() { + const onReady = new vscode.EventEmitter(); + let ready = false; + + const server = createServer(socket => { + if (ready) { + socket.end(); + } else { + onReady.event(() => socket.end()); + } + }); + + server.listen(0); + + return { + port: (server.address() as AddressInfo).port, + ready: () => { + ready = true; + onReady.fire(); + }, + dispose: () => { + server.close(); + }, + }; + } +} + +export class BrowserTestRunner extends VSCodeTestRunner { + /** @override */ + protected binaryPath(): Promise { + return Promise.resolve(process.execPath); + } + + /** @override */ + protected override getEnvironment() { + return { + ...super.getEnvironment(), + ELECTRON_RUN_AS_NODE: '1', + }; + } + + /** @override */ + protected getDefaultArgs() { + return [TEST_BROWSER_SCRIPT_PATH]; + } +} + +export class WindowsTestRunner extends VSCodeTestRunner { + /** @override */ + protected async binaryPath() { + const { nameShort } = await this.readProductJson(); + return path.join(this.repoLocation.uri.fsPath, `.build/electron/${nameShort}.exe`); + } + + /** @override */ + protected getDefaultArgs() { + return [TEST_ELECTRON_SCRIPT_PATH]; + } +} + +export class PosixTestRunner extends VSCodeTestRunner { + /** @override */ + protected async binaryPath() { + const { applicationName } = await this.readProductJson(); + return path.join(this.repoLocation.uri.fsPath, `.build/electron/${applicationName}`); + } + + /** @override */ + protected getDefaultArgs() { + return [TEST_ELECTRON_SCRIPT_PATH]; + } +} + +export class DarwinTestRunner extends PosixTestRunner { + /** @override */ + protected override getDefaultArgs() { + return [ + TEST_ELECTRON_SCRIPT_PATH, + '--no-sandbox', + '--disable-dev-shm-usage', + '--use-gl=swiftshader', + ]; + } + + /** @override */ + protected override async binaryPath() { + const { nameLong } = await this.readProductJson(); + return path.join( + this.repoLocation.uri.fsPath, + `.build/electron/${nameLong}.app/Contents/MacOS/Electron` + ); + } +} + +export const PlatformTestRunner = + process.platform === 'win32' + ? WindowsTestRunner + : process.platform === 'darwin' + ? DarwinTestRunner + : PosixTestRunner; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json new file mode 100644 index 00000000000..df98ed3e695 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../extensions/tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "types": [ + "node", + "mocha", + ] + }, + "include": [ + "src/**/*", + "../../../src/vscode-dts/vscode.d.ts", + "../../../src/vscode-dts/vscode.proposed.testObserver.d.ts", + "../../../src/vscode-dts/vscode.proposed.testMessageStackTrace.d.ts", + "../../../src/vscode-dts/vscode.proposed.testRelatedCode.d.ts", + "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts" + ] +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock new file mode 100644 index 00000000000..5dd9568bc9c --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -0,0 +1,60 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@types/istanbul-lib-coverage@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/mocha@^10.0.6": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" + integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== + +"@types/node@20.x": + version "20.12.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.11.tgz#c4ef00d3507000d17690643278a60dc55a9dc9be" + integrity sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw== + dependencies: + undici-types "~5.26.4" + +ansi-styles@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +cockatiel@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/cockatiel/-/cockatiel-3.1.3.tgz#bb1774a498a17e739dd994d56610dc6538b02858" + integrity sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ== + +istanbul-to-vscode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/istanbul-to-vscode/-/istanbul-to-vscode-2.0.1.tgz#84994d06e604b68cac7301840f338b1e74eb888b" + integrity sha512-V9Hhr7kX3UvkvkaT1lK3AmCRPkaIAIogQBrduTpNiLTkp1eVsybnJhWiDSVeCQap/3aGeZ2019oIivhX9MNsCQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.6" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 6b8a385ec42..c402cca3836 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:\"March 2024\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"August 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 750e53e4b26..450ba41835f 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:\"March 2024\"" + "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:\"August 2024\"" }, { "kind": 1, @@ -32,7 +32,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:bug label:insiders-released -label:verified -label:*duplicate -label:*as-designed -label:z-author-verified -label:on-testplan" + "value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:bug label:insiders-released -label:verified -label:*duplicate -label:*as-designed -label:z-author-verified -label:on-testplan -label:error-telemetry" }, { "kind": 1, @@ -42,7 +42,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:feature-request label:insiders-released -label:on-testplan -label:verified -label:*duplicate" + "value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:feature-request label:insiders-released -label:on-testplan -label:verified -label:*duplicate -label:error-telemetry" }, { "kind": 1, @@ -114,6 +114,16 @@ "language": "github-issues", "value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified -label:unreleased -label:*not-reproducible" }, + { + "kind": 1, + "language": "markdown", + "value": "## Verifiable Fixes Missing Steps" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug label:verification-steps-needed -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:z-author-verified -label:unreleased -label:*not-reproducible" + }, { "kind": 1, "language": "markdown", diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index ab59f23283f..df0f8a92998 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-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:\"March 2024\"\n\n$MINE=assignee:@me" + "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:\"August 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, @@ -157,7 +157,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:karrtikr -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:*out-of-scope -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer" }, { "kind": 1, @@ -167,7 +167,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed -author:@me sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -label:*not-reproducible" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed -author:@me sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -label:*not-reproducible -label:*out-of-scope" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index b23dacf87e4..979180e758a 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$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// current milestone name\n$MILESTONE=milestone:\"March 2024\"\n" + "value": "// list of repos we work in\n$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// current milestone name\n$MILESTONE=milestone:\"August 2024\"\n" }, { "kind": 1, @@ -82,7 +82,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode is:open assignee:@me label:triage-needed\n" + "value": "$REPOS is:open assignee:@me label:triage-needed\n" }, { "kind": 1, @@ -102,7 +102,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:command-center -label:comments -label:config -label:context-keys -label:custom-editors -label:debug -label:debug-console -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-sticky-scroll-decorations -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet-parse -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extension-signature -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:icon-brand -label:icons-product -label:icons-widget -label:inlay-hints -label:inline-chat -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:javascript -label:json -label:json-sorting -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:multi-monitor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-builtin-renderers -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-commands -label:notebook-commenting -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-math -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-rendering -label:notebook-serialization -label:notebook-serverless-web -label:notebook-statusbar -label:notebook-sticky-scroll -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:panel-chat -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:quickpick-chat -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-desktop -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-search -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:system-context-menu -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-tabs -label:terminal-winpty -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:unc -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-documents -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-auxwindow -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-feedback -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-voice -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:error-list -label:winget" + "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:command-center -label:comments -label:config -label:context-keys -label:custom-editors -label:debug -label:debug-console -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-sticky-scroll-decorations -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet-parse -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extension-signature -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:icon-brand -label:icons-product -label:icons-widget -label:inlay-hints -label:inline-chat -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:javascript -label:json -label:json-sorting -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:multi-monitor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-builtin-renderers -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-code-actions -label:notebook-commands -label:notebook-commenting -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-format -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-rendering -label:notebook-serialization -label:notebook-statusbar -label:notebook-sticky-scroll -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:panel-chat -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:quickpick-chat -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-desktop -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-search -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:system-context-menu -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-tabs -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:unc -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-documents -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-auxwindow -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-voice -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:error-list -label:winget" }, { "kind": 1, diff --git a/.vscode/notebooks/vscode-dev.github-issues b/.vscode/notebooks/vscode-dev.github-issues index 53fad82e6d9..db480b64bd8 100644 --- a/.vscode/notebooks/vscode-dev.github-issues +++ b/.vscode/notebooks/vscode-dev.github-issues @@ -2,7 +2,7 @@ { "kind": 2, "language": "github-issues", - "value": "$milestone=milestone:\"November 2023\"" + "value": "$milestone=milestone:\"August 2024\"" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index 121972d6ec6..d5123cb1e1b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,7 +30,7 @@ "test/automation/out/**": true, "test/integration/browser/out/**": true, "src/vs/base/test/common/filters.perf.data.js": true, - "src/vs/base/test/node/uri.test.data.txt": true, + "src/vs/base/test/node/uri.perf.data.txt": true, "src/vs/workbench/api/test/browser/extHostDocumentData.test.perf-data.ts": true, "src/vs/editor/test/node/diffing/fixtures/**": true, }, @@ -40,8 +40,6 @@ "**/Cargo.lock": true, "src/vs/workbench/workbench.web.main.css": true, "src/vs/workbench/workbench.desktop.main.css": true, - "src/vs/workbench/workbench.desktop.main.nls.js": true, - "src/vs/workbench/workbench.web.main.nls.js": true, "build/**/*.js": true, "out/**": true, "out-build/**": true, @@ -162,13 +160,12 @@ "@xterm/headless", "node-pty", "vscode-notebook-renderer", - "src/vs/workbench/workbench.web.main.ts", - "src/vs/workbench/api/common/extHostTypes.ts" + "src/vs/workbench/workbench.web.main.ts" ], "[github-issues]": { "editor.wordWrap": "on" }, "css.format.spaceAroundSelectorSeparator": true, "inlineChat.mode": "live", - "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/.yarnrc b/.yarnrc index 616968bddff..6d4a827644c 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "28.2.8" -ms_build_id "27744544" +target "30.1.2" +ms_build_id "9870757" runtime "electron" build_from_source "true" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 5f0de3e3365..0752f0a1f31 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -169,7 +169,7 @@ OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -atom/language-sass 0.61.4 - MIT +atom/language-sass 0.62.1 - MIT https://github.com/atom/language-sass The MIT License (MIT) @@ -517,7 +517,7 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -go-syntax 0.6.1 - MIT +go-syntax 0.7.5 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -777,7 +777,7 @@ SOFTWARE. --------------------------------------------------------- -jeff-hykin/better-shell-syntax 1.7.1 - MIT +jeff-hykin/better-shell-syntax 1.8.7 - MIT https://github.com/jeff-hykin/better-shell-syntax MIT License @@ -833,7 +833,7 @@ SOFTWARE. --------------------------------------------------------- -jlelong/vscode-latex-basics 1.6.0 - MIT +jlelong/vscode-latex-basics 1.9.0 - MIT https://github.com/jlelong/vscode-latex-basics Copyright (c) vscode-latex-basics authors @@ -1506,6 +1506,22 @@ SOFTWARE. --------------------------------------------------------- +RedCMD/YAML-Syntax-Highlighter 1.0.1 - MIT +https://github.com/RedCMD/YAML-Syntax-Highlighter + +MIT License + +Copyright 2024 RedCMD + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + redhat-developer/vscode-java 1.26.0 - MIT https://github.com/redhat-developer/vscode-java @@ -1928,31 +1944,6 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -textmate/yaml.tmbundle 0.0.0 - TextMate Bundle License -https://github.com/textmate/yaml.tmbundle - -Copyright (c) 2015 FichteFoll - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -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. ---------------------------------------------------------- - ---------------------------------------------------------- - trond-snekvik/vscode-rst 1.5.3 - MIT https://github.com/trond-snekvik/vscode-rst diff --git a/build/.cachesalt b/build/.cachesalt index 8051d84124e..dbeb17be7cf 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-03-18T08:47:22.277Z +2024-08-06T11:06:57.600Z diff --git a/build/.moduleignore b/build/.moduleignore index e40224556c6..97b504d8522 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -20,6 +20,15 @@ fsevents/test/** @vscode/spdlog/*.yml !@vscode/spdlog/build/Release/*.node +@vscode/deviceid/binding.gyp +@vscode/deviceid/build/** +@vscode/deviceid/deps/** +@vscode/deviceid/src/** +@vscode/deviceid/test/** +@vscode/deviceid/*.yml +!@vscode/deviceid/build/Release/*.node + + @vscode/sqlite3/binding.gyp @vscode/sqlite3/benchmark/** @vscode/sqlite3/cloudformation/** @@ -87,10 +96,13 @@ node-pty/lib/*.test.js node-pty/tools/** node-pty/deps/** node-pty/scripts/** +node-pty/third_party/** !node-pty/build/Release/spawn-helper !node-pty/build/Release/*.exe !node-pty/build/Release/*.dll !node-pty/build/Release/*.node +!node-pty/build/Release/conpty/conpty.dll +!node-pty/build/Release/conpty/OpenConsole.exe @parcel/watcher/binding.gyp @parcel/watcher/build/** diff --git a/build/.webignore b/build/.webignore index 0145e3b7da4..860ab59616b 100644 --- a/build/.webignore +++ b/build/.webignore @@ -20,8 +20,8 @@ vscode-textmate/webpack.config.js @xterm/xterm/src/** -@xterm/addon-canvas/src/** -@xterm/addon-canvas/out/** +@xterm/addon-clipboard/src/** +@xterm/addon-clipboard/out/** @xterm/addon-image/src/** @xterm/addon-image/out/** @@ -38,6 +38,7 @@ vscode-textmate/webpack.config.js # This makes sure the model is included in the package !@vscode/vscode-languagedetection/model/** +!@vscode/tree-sitter-wasm/wasm/** # Ensure only the required telemetry pieces are loaded in web to reduce bundle size @microsoft/1ds-core-js/** diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 267682f7f6d..e77ba78a999 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -49,16 +49,22 @@ steps: export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc" export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" export CC_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc --sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export PKG_CONFIG_LIBDIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu/pkgconfig:$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/share/pkgconfig" + export PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" export OBJDUMP="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/bin/objdump" elif [ "$SYSROOT_ARCH" == "amd64" ]; then export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc" export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -C link-arg=-L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu" export CC_x86_64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export PKG_CONFIG_LIBDIR_x86_64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu/pkgconfig:$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/share/pkgconfig" + export PKG_CONFIG_SYSROOT_DIR_x86_64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" export OBJDUMP="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/bin/objdump" elif [ "$SYSROOT_ARCH" == "armhf" ]; then export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc" export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" export CC_armv7_unknown_linux_gnueabihf="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc --sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export PKG_CONFIG_LIBDIR_armv7_unknown_linux_gnueabihf="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-rpi-linux-gnueabihf/pkgconfig:$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/share/pkgconfig" + export PKG_CONFIG_SYSROOT_DIR_armv7_unknown_linux_gnueabihf="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" export OBJDUMP="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/bin/objdump" fi fi @@ -78,7 +84,7 @@ steps: 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" + echo "Error: binary has dependency on GLIBC > 2.17, found $glibc_version" exit 1 fi fi diff --git a/build/azure-pipelines/cli/install-rust-posix.yml b/build/azure-pipelines/cli/install-rust-posix.yml index 78641cfb3a8..89867143938 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.73.0 + default: 1.77 - 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 3c88d9adc86..22fba8d7f6a 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.73.0 + default: 1.77 - name: targets default: [] type: object diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index c990e3a7146..aa185ed8369 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -389,14 +389,8 @@ function getPlatform(product, os, arch, type, isLegacy) { } } case 'server': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}`; case 'web': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}-web`; case 'cli': return `cli-win32-${arch}`; diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index 75065ffa2d3..652cd168335 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -550,14 +550,8 @@ function getPlatform(product: string, os: string, arch: string, type: string, is } } case 'server': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}`; case 'web': - if (arch === 'arm64') { - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } return `server-win32-${arch}-web`; case 'cli': return `cli-win32-${arch}`; diff --git a/build/azure-pipelines/config/CredScanSuppressions.json b/build/azure-pipelines/config/CredScanSuppressions.json index 96061477983..bf52c06cf89 100644 --- a/build/azure-pipelines/config/CredScanSuppressions.json +++ b/build/azure-pipelines/config/CredScanSuppressions.json @@ -10,9 +10,29 @@ }, { "file": [ - ".build/web/extensions/github-authentication/dist/browser/extension.js", - ".build/web/extensions/emmet/dist/browser/emmetBrowserMain.js.map", - ".build/web/extensions/emmet/dist/browser/emmetBrowserMain.js", + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/stage/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/stage/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/prime/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/prime/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/parts/code/build/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/parts/code/install/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/parts/code/src/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-x64/parts/code/build/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/parts/code/install/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-x64/parts/code/src/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js" + ], + "_justification": "These are safe to ignore, since they are built artifacts (stable)." + }, + { + "file": [ ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", @@ -32,7 +52,38 @@ ".build/linux/snap/x64/code-insiders-x64/parts/code/install/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/snap/x64/code-insiders-x64/parts/code/src/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js" ], - "_justification": "These are safe to ignore, since they are built artifacts." + "_justification": "These are safe to ignore, since they are built artifacts (insiders)." + }, + { + "file": [ + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/rpm/aarch64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/stage/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/stage/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/prime/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/prime/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/build/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/install/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/src/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/build/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/install/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/snap/x64/code-exploration-x64/parts/code/src/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js" + ], + "_justification": "These are safe to ignore, since they are built artifacts (exploration)." + }, + { + "file": [ + ".build/web/extensions/github-authentication/dist/browser/extension.js", + ".build/web/extensions/emmet/dist/browser/emmetBrowserMain.js.map", + ".build/web/extensions/emmet/dist/browser/emmetBrowserMain.js" + ], + "_justification": "These are safe to ignore, since they are built artifacts (web)." } ] } diff --git a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist index 1cc1a152c74..48f7bf5cece 100644 --- a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist +++ b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist @@ -6,8 +6,6 @@ com.apple.security.cs.allow-unsigned-executable-memory - com.apple.security.cs.allow-dyld-environment-variables - com.apple.security.cs.disable-library-validation diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index ed6d0236516..92fe6eb715e 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -59,7 +59,6 @@ steps: compile-extension:ipynb \ compile-extension:notebook-renderers \ compile-extension:json-language-features-server \ - compile-extension:markdown-language-features-server \ compile-extension:markdown-language-features \ compile-extension-media \ compile-extension:microsoft-authentication \ diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 11aa7605f63..11f69d735ac 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -78,8 +78,14 @@ steps: - script: | set -e - export npm_config_arch=$(VSCODE_ARCH) - npm i -g node-gyp@9.4.0 + # Refs https://github.com/microsoft/vscode/issues/219893#issuecomment-2209313109 + sudo xcode-select --switch /Applications/Xcode_15.2.app + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Switch to Xcode >= 15.1 + + - script: | + set -e + c++ --version python3 -m pip install setuptools for i in {1..5}; do # try 5 times @@ -91,6 +97,7 @@ steps: echo "Yarn failed $i, trying again..." done env: + npm_config_arch: $(VSCODE_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" diff --git a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml index dc8424f26ee..26f02657e10 100644 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -84,16 +84,6 @@ steps: imageName: vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) containerCommand: uname - - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: - - task: Docker@1 - displayName: "Pull Docker image" - inputs: - azureSubscriptionEndpoint: "vscode-builds-subscription" - azureContainerRegistry: vscodehub.azurecr.io - command: "Run an image" - imageName: vscode-linux-build-agent:bionic-arm32v7 - containerCommand: uname - - script: | set -e # To workaround the issue of yarn not respecting the registry value from .npmrc @@ -129,18 +119,8 @@ steps: 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) - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: - VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-arm32v7 displayName: Install dependencies - - ${{ 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 - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - - script: node build/azure-pipelines/distro/mixin-npm displayName: Mixin distro node modules @@ -172,9 +152,11 @@ steps: yarn 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 @@ -192,6 +174,26 @@ steps: 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: diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index f5c00aa0cf0..91cc411dd44 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -77,7 +77,6 @@ steps: compile-extension:ipynb \ compile-extension:notebook-renderers \ compile-extension:json-language-features-server \ - compile-extension:markdown-language-features-server \ compile-extension:markdown-language-features \ compile-extension-media \ compile-extension:microsoft-authentication \ @@ -151,7 +150,7 @@ steps: - script: yarn --cwd test/smoke compile displayName: Compile smoke tests - - script: yarn gulp compile-extension:markdown-language-features compile-extension-media compile-extension:vscode-test-resolver + - script: yarn gulp compile-extension:markdown-language-features compile-extension:ipynb compile-extension-media compile-extension:vscode-test-resolver displayName: Build extensions for smoke tests - script: yarn gulp node diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index cdc687fe7ac..d1d6bdb9191 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -100,50 +100,38 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication + - script: | + set -e + + for i in {1..5}; do # try 5 times + yarn --cwd build --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done + + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + yarn --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done + env: + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - script: | - set -e - # To workaround the issue of yarn not respecting the registry value from .npmrc - yarn config set registry "$NPM_REGISTRY" - - for i in {1..5}; do # try 5 times - yarn --cwd build --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - - source ./build/azure-pipelines/linux/setup-env.sh - - for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - env: - 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)" - displayName: Install dependencies (non-OSS) - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - - script: | - set -e - - EXPECTED_GLIBC_VERSION="2.28" \ - EXPECTED_GLIBCXX_VERSION="3.4.25" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - - script: node build/azure-pipelines/distro/mixin-npm condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules @@ -154,23 +142,12 @@ steps: - script: | set -e - for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - cd node_modules/native-keymap && npx node-gyp@9.4.0 -y rebuild --debug cd ../.. && ./.github/workflows/check-clean-git-state.sh env: npm_config_arch: $(NPM_ARCH) - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies (OSS) + displayName: Rebuild debug version of native modules (OSS) condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | @@ -226,9 +203,11 @@ steps: yarn 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-$(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 @@ -245,6 +224,36 @@ steps: 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 + + source ./build/azure-pipelines/linux/setup-env.sh + + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.25" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + + - ${{ else }}: + - script: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.26" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + - ${{ else }}: - script: yarn gulp "transpile-client-swc" "transpile-extensions" env: diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index e42a6b12b1f..949b5f371ba 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -13,7 +13,7 @@ SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } if [ "$npm_config_arch" == "x64" ]; then if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/120.0.6099.268/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/124.0.6367.243/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 \ @@ -25,12 +25,12 @@ if [ "$npm_config_arch" == "x64" ]; then # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243: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__ -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 --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -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 for remote server @@ -54,17 +54,15 @@ elif [ "$npm_config_arch" == "arm64" ]; then 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 elif [ "$npm_config_arch" == "arm" ]; then - if [ "$(echo "$@" | grep -c -- "--only-remote")" -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" + # 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 - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export VSCODE_REMOTE_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 remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export VSCODE_REMOTE_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 diff --git a/build/azure-pipelines/linux/verify-glibc-requirements.sh b/build/azure-pipelines/linux/verify-glibc-requirements.sh index f07c0ba71b0..19482c242ea 100755 --- a/build/azure-pipelines/linux/verify-glibc-requirements.sh +++ b/build/azure-pipelines/linux/verify-glibc-requirements.sh @@ -9,8 +9,8 @@ elif [ "$VSCODE_ARCH" == "armhf" ]; then TRIPLE="arm-rpi-linux-gnueabihf" fi -# Get all files with .node extension from remote/node_modules folder -files=$(find remote/node_modules -name "*.node" -not -path "*prebuilds*") +# Get all files with .node extension from server folder +files=$(find $SEARCH_PATH -name "*.node" -not -path "*prebuilds*" -o -type f -executable -name "node") echo "Verifying requirements for files: $files" @@ -19,13 +19,13 @@ for file in $files; do glibcxx_version="$EXPECTED_GLIBCXX_VERSION" while IFS= read -r line; do if [[ $line == *"GLIBC_"* ]]; then - version=$(echo "$line" | awk '{print $5}' | tr -d '()') + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') version=${version#*_} if [[ $(printf "%s\n%s" "$version" "$glibc_version" | sort -V | tail -n1) == "$version" ]]; then glibc_version=$version fi elif [[ $line == *"GLIBCXX_"* ]]; then - version=$(echo "$line" | awk '{print $5}' | tr -d '()') + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') version=${version#*_} if [[ $(printf "%s\n%s" "$version" "$glibcxx_version" | sort -V | tail -n1) == "$version" ]]; then glibcxx_version=$version @@ -34,11 +34,11 @@ for file in $files; do done < <("$PWD/.build/sysroots/$TRIPLE/$TRIPLE/bin/objdump" -T "$file") if [[ "$glibc_version" != "$EXPECTED_GLIBC_VERSION" ]]; then - echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION" + echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION, found $glibc_version" exit 1 fi if [[ "$glibcxx_version" != "$EXPECTED_GLIBCXX_VERSION" ]]; then - echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION" + echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION, found $glibcxx_version" exit 1 fi done diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 1d2d2b97506..0c4f98aa511 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -164,10 +164,10 @@ resources: - repository: 1ESPipelines type: git name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/heads/joao/disable-tsa-linux-arm64 + ref: refs/tags/release extends: - template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + template: v1/1ES.Unofficial.PipelineTemplate.yml@1esPipelines parameters: sdl: tsa: @@ -181,8 +181,16 @@ extends: notificationAliases: ['monacotools@microsoft.com'] validateToolOutput: None allTools: true + codeql: + compiled: + enabled: false + runSourceLanguagesInSourceAnalysis: true credscan: suppressionsFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/CredScanSuppressions.json + eslint: + enabled: true + enableExclusions: true + exclusionsFilePath: $(Build.SourcesDirectory)/.eslintignore sourceAnalysisPool: 1es-windows-2022-x64 containers: snapcraft: @@ -197,6 +205,7 @@ extends: - stage: Compile jobs: - job: Compile + timeoutInMinutes: 90 pool: name: 1es-ubuntu-20.04-x64 os: linux @@ -207,110 +216,124 @@ extends: parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - - stage: CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - stage: CompileCLI + dependsOn: [] + jobs: + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - job: CLILinuxX64 + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/linux/cli-build-linux.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_LINUX: ${{ parameters.VSCODE_BUILD_LINUX }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true))) }}: + - job: CLILinuxGnuARM + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/linux/cli-build-linux.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_LINUX_ARMHF: ${{ parameters.VSCODE_BUILD_LINUX_ARMHF }} + VSCODE_BUILD_LINUX_ARM64: ${{ parameters.VSCODE_BUILD_LINUX_ARM64 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE, true)) }}: + - job: CLIAlpineX64 + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_ALPINE: ${{ parameters.VSCODE_BUILD_ALPINE }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }}: + - job: CLIAlpineARM64 + pool: + name: 1es-mariner-2.0-arm64 + os: linux + hostArchitecture: arm64 + container: ubuntu-2004-arm64 + steps: + - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_ALPINE_ARM64: ${{ parameters.VSCODE_BUILD_ALPINE_ARM64 }} + + - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: + - job: CLIMacOSX64 + pool: + name: Azure Pipelines + image: macOS-13 + os: macOS + steps: + - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: + - job: CLIMacOSARM64 + pool: + name: Azure Pipelines + image: macOS-13 + os: macOS + steps: + - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} + + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - job: CLIWindowsX64 + pool: + name: 1es-windows-2019-x64 + os: windows + steps: + - template: build/azure-pipelines/win32/cli-build-win32.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - job: CLIWindowsARM64 + pool: + name: 1es-windows-2019-x64 + os: windows + steps: + - template: build/azure-pipelines/win32/cli-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + + - stage: CustomSDL dependsOn: [] + pool: + name: 1es-windows-2019-x64 + os: windows jobs: - - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: - - job: CLILinuxX64 - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - steps: - - template: build/azure-pipelines/linux/cli-build-linux.yml@self - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_LINUX: ${{ parameters.VSCODE_BUILD_LINUX }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true))) }}: - - job: CLILinuxGnuARM - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - steps: - - template: build/azure-pipelines/linux/cli-build-linux.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_LINUX_ARMHF: ${{ parameters.VSCODE_BUILD_LINUX_ARMHF }} - VSCODE_BUILD_LINUX_ARM64: ${{ parameters.VSCODE_BUILD_LINUX_ARM64 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE, true)) }}: - - job: CLIAlpineX64 - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - steps: - - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_ALPINE: ${{ parameters.VSCODE_BUILD_ALPINE }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }}: - - job: CLIAlpineARM64 - pool: - name: 1es-mariner-2.0-arm64 - os: linux - hostArchitecture: arm64 - container: ubuntu-2004-arm64 - steps: - - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_ALPINE_ARM64: ${{ parameters.VSCODE_BUILD_ALPINE_ARM64 }} - - - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - - job: CLIMacOSX64 - pool: - name: Azure Pipelines - image: macOS-11 - os: macOS - steps: - - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - - job: CLIMacOSARM64 - pool: - name: Azure Pipelines - image: macOS-11 - os: macOS - steps: - - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} - - - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - job: CLIWindowsX64 - pool: - name: 1es-windows-2019-x64 - os: windows - steps: - - template: build/azure-pipelines/win32/cli-build-win32.yml@self - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - - job: CLIWindowsARM64 - pool: - name: 1es-windows-2019-x64 - os: windows - steps: - - template: build/azure-pipelines/win32/cli-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + - job: WindowsSDL + variables: + - group: 'API Scan' + steps: + - template: build/azure-pipelines/sdl-scan.yml@self - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true)) }}: - stage: Windows dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: 1es-windows-2019-x64 os: windows @@ -401,7 +424,8 @@ extends: - stage: Linux dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: 1es-ubuntu-20.04-x64 os: linux @@ -455,6 +479,7 @@ extends: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: - job: Linuxx64 + timeoutInMinutes: 90 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -558,7 +583,8 @@ extends: - stage: Alpine dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: 1es-ubuntu-20.04-x64 os: linux @@ -584,10 +610,11 @@ extends: - stage: macOS dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: Azure Pipelines - image: macOS-11 + image: macOS-13 os: macOS variables: BUILDSECMON_OPT_IN: true diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 5fd12caf017..9a3748ed6fc 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -104,11 +104,13 @@ steps: - script: yarn npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 displayName: Compile & Hygiene (OSS) - ${{ else }}: - script: yarn npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 displayName: Compile & Hygiene (non-OSS) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: @@ -133,13 +135,22 @@ steps: - script: | set -e - AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_STORAGE_ACCOUNT="vscodeweb" \ AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ node build/azure-pipelines/upload-sourcemaps displayName: Upload sourcemaps to Azure + - script: | + set -e + AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ + AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ + AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ + node build/azure-pipelines/upload-sourcemaps + displayName: Upload sourcemaps to Azure (Deprecated) + - script: ./build/azure-pipelines/common/extract-telemetry.sh displayName: Generate lists of telemetry events diff --git a/build/azure-pipelines/sdl-scan.yml b/build/azure-pipelines/sdl-scan.yml index 927cd5e04ae..c28755b0522 100644 --- a/build/azure-pipelines/sdl-scan.yml +++ b/build/azure-pipelines/sdl-scan.yml @@ -1,296 +1,153 @@ -trigger: none -pr: none - parameters: - name: NPM_REGISTRY displayName: "Custom NPM Registry" type: string default: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/" - - name: SCAN_WINDOWS - displayName: "Scan Windows" - type: boolean - default: true - - name: SCAN_LINUX - displayName: "Scan Linux" - type: boolean - default: false - -variables: - - name: NPM_REGISTRY - value: ${{ parameters.NPM_REGISTRY }} - - name: SCAN_WINDOWS - value: ${{ eq(parameters.SCAN_WINDOWS, true) }} - - name: SCAN_LINUX - value: ${{ eq(parameters.SCAN_LINUX, true) }} - - name: VSCODE_MIXIN_REPO - value: microsoft/vscode-distro - - name: skipComponentGovernanceDetection - value: true - name: NPM_ARCH - value: x64 + type: string + default: x64 - name: VSCODE_ARCH - value: x64 - - name: Codeql.enabled - value: true - - name: Codeql.TSAEnabled - value: true - - name: Codeql.TSAOptionsPath - value: '$(Build.SourcesDirectory)\build\azure-pipelines\config\tsaoptions.json' + type: string + default: x64 -stages: - - stage: Windows - condition: eq(variables.SCAN_WINDOWS, 'true') - pool: 1es-windows-2019-x64 - jobs: - - job: WindowsJob - timeoutInMinutes: 0 - steps: - - task: CredScan@3 - continueOnError: true - inputs: - scanFolder: "$(Build.SourcesDirectory)" - outputFormat: "pre" +steps: + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + - template: ./distro/download-distro.yml - - template: ./distro/download-distro.yml + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm config set registry "${{ parameters.NPM_REGISTRY }}" --location=project } + # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb + # following is a workaround for yarn to send authorization header + # for GET requests to the registry. + exec { Add-Content -Path .npmrc -Value "always-auth=true" } + exec { yarn config set registry "${{ parameters.NPM_REGISTRY }}" } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM & Yarn - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm config set registry "$env:NPM_REGISTRY" --location=project } - # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb - # following is a workaround for yarn to send authorization header - # for GET requests to the registry. - exec { Add-Content -Path .npmrc -Value "always-auth=true" } - exec { yarn config set registry "$env:NPM_REGISTRY" } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM & Yarn + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM Authentication - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build/setup-npm-registry.js "${{ parameters.NPM_REGISTRY }}" } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM Registry - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build/setup-npm-registry.js $env:NPM_REGISTRY } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry + - pwsh: | + $includes = @' + { + 'target_defaults': { + 'conditions': [ + ['OS=="win"', { + 'msvs_configuration_attributes': { + 'SpectreMitigation': 'Spectre' + }, + 'msvs_settings': { + 'VCCLCompilerTool': { + 'AdditionalOptions': [ + '/Zi', + '/FS' + ], + }, + 'VCLinkerTool': { + 'AdditionalOptions': [ + '/profile' + ] + } + } + }] + ] + } + } + '@ - - task: CodeQL3000Init@0 - displayName: CodeQL Initialize - condition: eq(variables['Codeql.enabled'], 'True') + if (!(Test-Path "~/.gyp")) { + mkdir "~/.gyp" + } + echo $includes > "~/.gyp/include.gypi" + displayName: Create include.gypi - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - . build/azure-pipelines/win32/retry.ps1 - $ErrorActionPreference = "Stop" - # TODO: remove custom node-gyp when updating to Node v20, - # refs https://github.com/npm/cli/releases/tag/v10.2.3 which is available with Node >= 20.10.0 - $nodeGypDir = "$(Agent.TempDirectory)/custom-packages" - mkdir "$nodeGypDir" - npm install node-gyp@10.0.1 -g --prefix "$nodeGypDir" - $env:npm_config_node_gyp = "${nodeGypDir}/node_modules/node-gyp/bin/node-gyp.js" - $env:npm_config_arch = "$(NPM_ARCH)" - retry { exec { yarn --frozen-lockfile --check-files } } - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - CHILD_CONCURRENCY: 1 - displayName: Install dependencies + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + . build/azure-pipelines/win32/retry.ps1 + $ErrorActionPreference = "Stop" + retry { exec { yarn --frozen-lockfile --check-files } } + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + CHILD_CONCURRENCY: 1 + displayName: Install dependencies - - script: node build/azure-pipelines/distro/mixin-npm - displayName: Mixin distro node modules + - 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 - env: - VSCODE_QUALITY: stable + - script: node build/azure-pipelines/distro/mixin-quality + displayName: Mixin distro quality + env: + VSCODE_QUALITY: stable - - powershell: yarn compile - displayName: Compile + - powershell: yarn compile + displayName: Compile - - task: CodeQL3000Finalize@0 - displayName: CodeQL Finalize - condition: eq(variables['Codeql.enabled'], 'True') + - powershell: yarn gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Download Symbols - - powershell: yarn gulp "vscode-symbols-win32-$(VSCODE_ARCH)" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Download Symbols + - 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' - - task: PSScriptAnalyzer@1 - inputs: - Path: '$(Build.SourcesDirectory)' - Settings: required - Recurse: true + - task: CopyFiles@2 + displayName: 'Collect Symbols for API Scan' + inputs: + SourceFolder: $(Agent.BuildDirectory) + Contents: 'scanbin\**\*.pdb' + TargetFolder: '$(agent.builddirectory)\symbols' + flattenFolders: true + condition: succeeded() - - 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-$(VSCODE_ARCH)\pdb' + - task: APIScan@2 + inputs: + softwareFolder: $(agent.builddirectory)\scanbin + softwareName: 'vscode-client' + softwareVersionNum: '1' + symbolsFolder: 'srv*https://symweb.azurefd.net;$(agent.builddirectory)\symbols' + isLargeApp: false + toolVersion: 'Latest' + azureSubscription: 'vscode-apiscan' + displayName: Run ApiScan + condition: succeeded() + env: + AzureServicesAuthConnectionString: $(apiscan-connectionstring) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - - task: AntiMalware@4 - inputs: - InputType: Basic - ScanType: CustomScan - FileDirPath: '$(Build.SourcesDirectory)' - EnableServices: true - SupportLogOnError: false - TreatSignatureUpdateFailureAs: 'Warning' - SignatureFreshness: 'OneDay' - TreatStaleSignatureAs: 'Error' - - - task: PublishSecurityAnalysisLogs@3 - inputs: - ArtifactName: CodeAnalysisLogs - ArtifactType: Container - PublishProcessedResults: false - AllTools: true - - - task: TSAUpload@2 - inputs: - GdnPublishTsaOnboard: true - GdnPublishTsaConfigFile: '$(Build.SourcesDirectory)\build\azure-pipelines\config\tsaoptions.json' - - - stage: Linux - dependsOn: [] - condition: eq(variables.SCAN_LINUX, 'true') - pool: - vmImage: "Ubuntu-18.04" - jobs: - - job: LinuxJob - steps: - - task: CredScan@2 - inputs: - toolMajorVersion: "V2" - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - template: ./distro/download-distro.yml - - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - script: | - set -e - npm config set registry "$NPM_REGISTRY" --location=project - # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb - # following is a workaround for yarn to send authorization header - # for GET requests to the registry. - echo "always-auth=true" >> .npmrc - yarn config set registry "$NPM_REGISTRY" - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM & Yarn - - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - script: node build/setup-npm-registry.js $NPM_REGISTRY - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - script: | - set -e - for i in {1..5}; do # try 5 times - yarn --cwd build --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - displayName: Install build dependencies - - - script: | - set -e - export npm_config_arch=$(NPM_ARCH) - - if [ -z "$CC" ] || [ -z "$CXX" ]; then - # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/96.0.4664.110/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_ARCH)" \ - node build/linux/libcxx-fetcher.js - # Set compiler toolchain - export CC=$PWD/.build/CR_Clang/bin/clang - export CXX=$PWD/.build/CR_Clang/bin/clang++ - export CXXFLAGS="-std=c++17 -nostdinc++ -D__NO_INLINE__ -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" - export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -fsplit-lto-unit -L$PWD/.build/libcxx-objects -lc++abi" - export VSCODE_REMOTE_CC=$(which gcc) - export VSCODE_REMOTE_CXX=$(which g++) - fi - - for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies - - - script: yarn --frozen-lockfile --check-files - workingDirectory: .build/distro/npm - env: - npm_config_arch: $(NPM_ARCH) - displayName: Install distro node modules - - - 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 - env: - VSCODE_QUALITY: stable - - - script: yarn gulp vscode-symbols-linux-$(VSCODE_ARCH) - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build - - - task: BinSkim@3 - inputs: - toolVersion: Latest - InputType: CommandLine - arguments: analyze $(agent.builddirectory)\scanbin\exe\*.* --recurse --local-symbol-directories $(agent.builddirectory)\scanbin\VSCode-linux-$(VSCODE_ARCH)\pdb - - - task: TSAUpload@2 - inputs: - GdnPublishTsaConfigFile: '$(Build.SourceDirectory)\build\azure-pipelines\config\tsaoptions.json' + - task: PublishSecurityAnalysisLogs@3 + inputs: + ArtifactName: CodeAnalysisLogs + ArtifactType: Container + PublishProcessedResults: false + AllTools: true diff --git a/build/azure-pipelines/upload-configuration.js b/build/azure-pipelines/upload-configuration.js deleted file mode 100644 index 39a44dc5c41..00000000000 --- a/build/azure-pipelines/upload-configuration.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getSettingsSearchBuildId = exports.shouldSetupSettingsSearch = void 0; -const path = require("path"); -const os = require("os"); -const cp = require("child_process"); -const vfs = require("vinyl-fs"); -const util = require("../lib/util"); -const identity_1 = require("@azure/identity"); -const azure = require('gulp-azure-storage'); -const packageJson = require("../../package.json"); -const commit = process.env['BUILD_SOURCEVERSION']; -function generateVSCodeConfigurationTask() { - return new Promise((resolve, reject) => { - const buildDir = process.env['AGENT_BUILDDIRECTORY']; - if (!buildDir) { - return reject(new Error('$AGENT_BUILDDIRECTORY not set')); - } - if (!shouldSetupSettingsSearch()) { - console.log(`Only runs on main and release branches, not ${process.env.BUILD_SOURCEBRANCH}`); - return resolve(undefined); - } - if (process.env.VSCODE_QUALITY !== 'insider' && process.env.VSCODE_QUALITY !== 'stable') { - console.log(`Only runs on insider and stable qualities, not ${process.env.VSCODE_QUALITY}`); - return resolve(undefined); - } - const result = path.join(os.tmpdir(), 'configuration.json'); - const userDataDir = path.join(os.tmpdir(), 'tmpuserdata'); - const extensionsDir = path.join(os.tmpdir(), 'tmpextdir'); - const arch = process.env['VSCODE_ARCH']; - const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); - const appName = process.env.VSCODE_QUALITY === 'insider' ? 'Visual\\ Studio\\ Code\\ -\\ Insiders.app' : 'Visual\\ Studio\\ Code.app'; - const appPath = path.join(appRoot, appName, 'Contents', 'Resources', 'app', 'bin', 'code'); - const codeProc = cp.exec(`${appPath} --export-default-configuration='${result}' --wait --user-data-dir='${userDataDir}' --extensions-dir='${extensionsDir}'`, (err, stdout, stderr) => { - clearTimeout(timer); - if (err) { - console.log(`err: ${err} ${err.message} ${err.toString()}`); - reject(err); - } - if (stdout) { - console.log(`stdout: ${stdout}`); - } - if (stderr) { - console.log(`stderr: ${stderr}`); - } - resolve(result); - }); - const timer = setTimeout(() => { - codeProc.kill(); - reject(new Error('export-default-configuration process timed out')); - }, 60 * 1000); - codeProc.on('error', err => { - clearTimeout(timer); - reject(err); - }); - }); -} -function shouldSetupSettingsSearch() { - const branch = process.env.BUILD_SOURCEBRANCH; - return !!(branch && (/\/main$/.test(branch) || branch.indexOf('/release/') >= 0)); -} -exports.shouldSetupSettingsSearch = shouldSetupSettingsSearch; -function getSettingsSearchBuildId(packageJson) { - try { - const branch = process.env.BUILD_SOURCEBRANCH; - const branchId = branch.indexOf('/release/') >= 0 ? 0 : - /\/main$/.test(branch) ? 1 : - 2; // Some unexpected branch - const out = cp.execSync(`git rev-list HEAD --count`); - const count = parseInt(out.toString()); - // - // 1.25.1, 1,234,567 commits, main = 1250112345671 - return util.versionStringToNumber(packageJson.version) * 1e8 + count * 10 + branchId; - } - catch (e) { - throw new Error('Could not determine build number: ' + e.toString()); - } -} -exports.getSettingsSearchBuildId = getSettingsSearchBuildId; -async function main() { - const configPath = await generateVSCodeConfigurationTask(); - if (!configPath) { - return; - } - const settingsSearchBuildId = getSettingsSearchBuildId(packageJson); - if (!settingsSearchBuildId) { - throw new Error('Failed to compute build number'); - } - const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); - return new Promise((c, e) => { - vfs.src(configPath) - .pipe(azure.upload({ - account: process.env.AZURE_STORAGE_ACCOUNT, - credential, - container: 'configuration', - prefix: `${settingsSearchBuildId}/${commit}/` - })) - .on('end', () => c()) - .on('error', (err) => e(err)); - }); -} -if (require.main === module) { - main().catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBsb2FkLWNvbmZpZ3VyYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ1cGxvYWQtY29uZmlndXJhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7OztnR0FHZ0c7OztBQUVoRyw2QkFBNkI7QUFDN0IseUJBQXlCO0FBQ3pCLG9DQUFvQztBQUNwQyxnQ0FBZ0M7QUFDaEMsb0NBQW9DO0FBQ3BDLDhDQUF5RDtBQUN6RCxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsb0JBQW9CLENBQUMsQ0FBQztBQUM1QyxrREFBa0Q7QUFFbEQsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO0FBRWxELFNBQVMsK0JBQStCO0lBQ3ZDLE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDdEMsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDO1FBQ3JELElBQUksQ0FBQyxRQUFRLEVBQUU7WUFDZCxPQUFPLE1BQU0sQ0FBQyxJQUFJLEtBQUssQ0FBQywrQkFBK0IsQ0FBQyxDQUFDLENBQUM7U0FDMUQ7UUFFRCxJQUFJLENBQUMseUJBQXlCLEVBQUUsRUFBRTtZQUNqQyxPQUFPLENBQUMsR0FBRyxDQUFDLCtDQUErQyxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQixFQUFFLENBQUMsQ0FBQztZQUM3RixPQUFPLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQztTQUMxQjtRQUVELElBQUksT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEtBQUssU0FBUyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxLQUFLLFFBQVEsRUFBRTtZQUN4RixPQUFPLENBQUMsR0FBRyxDQUFDLGtEQUFrRCxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsRUFBRSxDQUFDLENBQUM7WUFDNUYsT0FBTyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUM7U0FDMUI7UUFFRCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxNQUFNLEVBQUUsRUFBRSxvQkFBb0IsQ0FBQyxDQUFDO1FBQzVELE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLGFBQWEsQ0FBQyxDQUFDO1FBQzFELE1BQU0sYUFBYSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLFdBQVcsQ0FBQyxDQUFDO1FBQzFELE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDeEMsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsaUJBQWlCLElBQUksRUFBRSxDQUFDLENBQUM7UUFDN0QsTUFBTSxPQUFPLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDLENBQUMsNEJBQTRCLENBQUM7UUFDdEksTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQztRQUMzRixNQUFNLFFBQVEsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUN2QixHQUFHLE9BQU8sb0NBQW9DLE1BQU0sNkJBQTZCLFdBQVcsdUJBQXVCLGFBQWEsR0FBRyxFQUNuSSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDdkIsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO1lBQ3BCLElBQUksR0FBRyxFQUFFO2dCQUNSLE9BQU8sQ0FBQyxHQUFHLENBQUMsUUFBUSxHQUFHLElBQUksR0FBRyxDQUFDLE9BQU8sSUFBSSxHQUFHLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO2dCQUM1RCxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7YUFDWjtZQUVELElBQUksTUFBTSxFQUFFO2dCQUNYLE9BQU8sQ0FBQyxHQUFHLENBQUMsV0FBVyxNQUFNLEVBQUUsQ0FBQyxDQUFDO2FBQ2pDO1lBRUQsSUFBSSxNQUFNLEVBQUU7Z0JBQ1gsT0FBTyxDQUFDLEdBQUcsQ0FBQyxXQUFXLE1BQU0sRUFBRSxDQUFDLENBQUM7YUFDakM7WUFFRCxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDakIsQ0FBQyxDQUNELENBQUM7UUFDRixNQUFNLEtBQUssR0FBRyxVQUFVLENBQUMsR0FBRyxFQUFFO1lBQzdCLFFBQVEsQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUNoQixNQUFNLENBQUMsSUFBSSxLQUFLLENBQUMsZ0RBQWdELENBQUMsQ0FBQyxDQUFDO1FBQ3JFLENBQUMsRUFBRSxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUM7UUFFZCxRQUFRLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxHQUFHLENBQUMsRUFBRTtZQUMxQixZQUFZLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDcEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ2IsQ0FBQyxDQUFDLENBQUM7SUFDSixDQUFDLENBQUMsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFnQix5QkFBeUI7SUFDeEMsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsQ0FBQztJQUM5QyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ25GLENBQUM7QUFIRCw4REFHQztBQUVELFNBQWdCLHdCQUF3QixDQUFDLFdBQWdDO0lBQ3hFLElBQUk7UUFDSCxNQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFtQixDQUFDO1FBQy9DLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUN0RCxTQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztnQkFDM0IsQ0FBQyxDQUFDLENBQUMseUJBQXlCO1FBRTlCLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxRQUFRLENBQUMsMkJBQTJCLENBQUMsQ0FBQztRQUNyRCxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsR0FBRyxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFFdkMsc0VBQXNFO1FBQ3RFLGtEQUFrRDtRQUNsRCxPQUFPLElBQUksQ0FBQyxxQkFBcUIsQ0FBQyxXQUFXLENBQUMsT0FBTyxDQUFDLEdBQUcsR0FBRyxHQUFHLEtBQUssR0FBRyxFQUFFLEdBQUcsUUFBUSxDQUFDO0tBQ3JGO0lBQUMsT0FBTyxDQUFDLEVBQUU7UUFDWCxNQUFNLElBQUksS0FBSyxDQUFDLG9DQUFvQyxHQUFHLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0tBQ3JFO0FBQ0YsQ0FBQztBQWhCRCw0REFnQkM7QUFFRCxLQUFLLFVBQVUsSUFBSTtJQUNsQixNQUFNLFVBQVUsR0FBRyxNQUFNLCtCQUErQixFQUFFLENBQUM7SUFFM0QsSUFBSSxDQUFDLFVBQVUsRUFBRTtRQUNoQixPQUFPO0tBQ1A7SUFFRCxNQUFNLHFCQUFxQixHQUFHLHdCQUF3QixDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRXBFLElBQUksQ0FBQyxxQkFBcUIsRUFBRTtRQUMzQixNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUM7S0FDbEQ7SUFFRCxNQUFNLFVBQVUsR0FBRyxJQUFJLGlDQUFzQixDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLENBQUUsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLGlCQUFpQixDQUFFLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBRSxDQUFDLENBQUM7SUFFckosT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRTtRQUMzQixHQUFHLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQzthQUNqQixJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQztZQUNsQixPQUFPLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUI7WUFDMUMsVUFBVTtZQUNWLFNBQVMsRUFBRSxlQUFlO1lBQzFCLE1BQU0sRUFBRSxHQUFHLHFCQUFxQixJQUFJLE1BQU0sR0FBRztTQUM3QyxDQUFDLENBQUM7YUFDRixFQUFFLENBQUMsS0FBSyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDO2FBQ3BCLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxHQUFRLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQ3JDLENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQUVELElBQUksT0FBTyxDQUFDLElBQUksS0FBSyxNQUFNLEVBQUU7SUFDNUIsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxFQUFFO1FBQ2xCLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDbkIsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNqQixDQUFDLENBQUMsQ0FBQztDQUNIIn0= \ No newline at end of file diff --git a/build/azure-pipelines/upload-nlsmetadata.js b/build/azure-pipelines/upload-nlsmetadata.js index 34c2005a30f..5b6cd3ed1fd 100644 --- a/build/azure-pipelines/upload-nlsmetadata.js +++ b/build/azure-pipelines/upload-nlsmetadata.js @@ -16,13 +16,33 @@ const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); function main() { return new Promise((c, e) => { - es.merge(vfs.src('out-vscode-web-min/nls.metadata.json', { base: 'out-vscode-web-min' }), vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })) + const combinedMetadataJson = es.merge( + // vscode: we are not using `out-build/nls.metadata.json` here because + // it includes metadata for translators for `keys`. but for our purpose + // we want only the `keys` and `messages` as `string`. + es.merge(vfs.src('out-build/nls.keys.json', { base: 'out-build' }), vfs.src('out-build/nls.messages.json', { base: 'out-build' })) .pipe(merge({ + fileName: 'vscode.json', + jsonSpace: '', + concatArrays: true, + edit: (parsedJson, file) => { + if (file.base === 'out-build') { + if (file.basename === 'nls.keys.json') { + return { keys: parsedJson }; + } + else { + return { messages: parsedJson }; + } + } + } + })), + // extensions + vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe(merge({ fileName: 'combined.nls.metadata.json', jsonSpace: '', concatArrays: true, edit: (parsedJson, file) => { - if (file.base === 'out-vscode-web-min') { + if (file.basename === 'vscode.json') { return { vscode: parsedJson }; } // Handle extensions and follow the same structure as the Core nls file. @@ -72,13 +92,15 @@ function main() { const key = manifestJson.publisher + '.' + manifestJson.name; return { [key]: parsedJson }; }, - })) + })); + const nlsMessagesJs = vfs.src('out-build/nls.messages.js', { base: 'out-build' }); + es.merge(combinedMetadataJson, nlsMessagesJs) .pipe(gzip({ append: false })) .pipe(vfs.dest('./nlsMetadata')) .pipe(es.through(function (data) { console.log(`Uploading ${data.path}`); // trigger artifact upload - console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=combined.nls.metadata.json]${data.path}`); + console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=${data.basename}]${data.path}`); this.emit('data', data); })) .pipe(azure.upload({ diff --git a/build/azure-pipelines/upload-nlsmetadata.ts b/build/azure-pipelines/upload-nlsmetadata.ts index 416d0eec408..030cc8f0e5a 100644 --- a/build/azure-pipelines/upload-nlsmetadata.ts +++ b/build/azure-pipelines/upload-nlsmetadata.ts @@ -24,79 +24,103 @@ interface NlsMetadata { function main(): Promise { return new Promise((c, e) => { - - es.merge( - vfs.src('out-vscode-web-min/nls.metadata.json', { base: 'out-vscode-web-min' }), - vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), - vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), - vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })) - .pipe(merge({ - fileName: 'combined.nls.metadata.json', - jsonSpace: '', - concatArrays: true, - edit: (parsedJson, file) => { - if (file.base === 'out-vscode-web-min') { - return { vscode: parsedJson }; - } - - // Handle extensions and follow the same structure as the Core nls file. - switch (file.basename) { - case 'package.nls.json': - // put package.nls.json content in Core NlsMetadata format - // language packs use the key "package" to specify that - // translations are for the package.json file - parsedJson = { - messages: { - package: Object.values(parsedJson) - }, - keys: { - package: Object.keys(parsedJson) - }, - bundles: { - main: ['package'] - } - }; - break; - - case 'nls.metadata.header.json': - parsedJson = { header: parsedJson }; - break; - - case 'nls.metadata.json': { - // put nls.metadata.json content in Core NlsMetadata format - const modules = Object.keys(parsedJson); - - const json: NlsMetadata = { - keys: {}, - messages: {}, - bundles: { - main: [] - } - }; - for (const module of modules) { - json.messages[module] = parsedJson[module].messages; - json.keys[module] = parsedJson[module].keys; - json.bundles.main.push(module); + const combinedMetadataJson = es.merge( + // vscode: we are not using `out-build/nls.metadata.json` here because + // it includes metadata for translators for `keys`. but for our purpose + // we want only the `keys` and `messages` as `string`. + es.merge( + vfs.src('out-build/nls.keys.json', { base: 'out-build' }), + vfs.src('out-build/nls.messages.json', { base: 'out-build' })) + .pipe(merge({ + fileName: 'vscode.json', + jsonSpace: '', + concatArrays: true, + edit: (parsedJson, file) => { + if (file.base === 'out-build') { + if (file.basename === 'nls.keys.json') { + return { keys: parsedJson }; + } else { + return { messages: parsedJson }; } - parsedJson = json; - break; } } + })), - // Get extension id and use that as the key - const folderPath = path.join(file.base, file.relative.split('/')[0]); - const manifest = readFileSync(path.join(folderPath, 'package.json'), 'utf-8'); - const manifestJson = JSON.parse(manifest); - const key = manifestJson.publisher + '.' + manifestJson.name; - return { [key]: parsedJson }; - }, - })) + // extensions + vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), + vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), + vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' }) + ).pipe(merge({ + fileName: 'combined.nls.metadata.json', + jsonSpace: '', + concatArrays: true, + edit: (parsedJson, file) => { + if (file.basename === 'vscode.json') { + return { vscode: parsedJson }; + } + + // Handle extensions and follow the same structure as the Core nls file. + switch (file.basename) { + case 'package.nls.json': + // put package.nls.json content in Core NlsMetadata format + // language packs use the key "package" to specify that + // translations are for the package.json file + parsedJson = { + messages: { + package: Object.values(parsedJson) + }, + keys: { + package: Object.keys(parsedJson) + }, + bundles: { + main: ['package'] + } + }; + break; + + case 'nls.metadata.header.json': + parsedJson = { header: parsedJson }; + break; + + case 'nls.metadata.json': { + // put nls.metadata.json content in Core NlsMetadata format + const modules = Object.keys(parsedJson); + + const json: NlsMetadata = { + keys: {}, + messages: {}, + bundles: { + main: [] + } + }; + for (const module of modules) { + json.messages[module] = parsedJson[module].messages; + json.keys[module] = parsedJson[module].keys; + json.bundles.main.push(module); + } + parsedJson = json; + break; + } + } + + // Get extension id and use that as the key + const folderPath = path.join(file.base, file.relative.split('/')[0]); + const manifest = readFileSync(path.join(folderPath, 'package.json'), 'utf-8'); + const manifestJson = JSON.parse(manifest); + const key = manifestJson.publisher + '.' + manifestJson.name; + return { [key]: parsedJson }; + }, + })); + + const nlsMessagesJs = vfs.src('out-build/nls.messages.js', { base: 'out-build' }); + + es.merge(combinedMetadataJson, nlsMessagesJs) .pipe(gzip({ append: false })) .pipe(vfs.dest('./nlsMetadata')) .pipe(es.through(function (data: Vinyl) { console.log(`Uploading ${data.path}`); // trigger artifact upload - console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=combined.nls.metadata.json]${data.path}`); + console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=${data.basename}]${data.path}`); this.emit('data', data); })) .pipe(azure.upload({ diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 72ded6bcc11..a522e845f3b 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -104,6 +104,7 @@ steps: tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-web echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build @@ -128,6 +129,15 @@ steps: node build/azure-pipelines/upload-cdn displayName: Upload to CDN + - script: | + set -e + AZURE_STORAGE_ACCOUNT="vscodeweb" \ + AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ + AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ + AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ + node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.js.map + displayName: Upload sourcemaps (Web) + # upload only the workbench.web.main.js source maps because # we just compiled these bits in the previous step and the # general task to upload source maps has already been run @@ -138,11 +148,11 @@ steps: AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.js.map - displayName: Upload sourcemaps (Web) + displayName: Upload sourcemaps (Deprecated) - script: | set -e - AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_STORAGE_ACCOUNT="vscodeweb" \ AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index a3b251b71ac..c9b2d6cfa61 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -61,7 +61,6 @@ steps: compile-extension:ipynb ` compile-extension:notebook-renderers ` compile-extension:json-language-features-server ` - compile-extension:markdown-language-features-server ` compile-extension:markdown-language-features ` compile-extension-media ` compile-extension:microsoft-authentication ` @@ -72,6 +71,11 @@ steps: } displayName: Build integration tests + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics before integration test runs + continueOnError: true + condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - powershell: .\scripts\test-integration.bat --tfs "Integration Tests" displayName: Run integration tests (Electron) @@ -121,6 +125,11 @@ steps: displayName: Run integration tests (Remote) timeoutInMinutes: 20 + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics after integration test runs + continueOnError: true + condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - powershell: .\build\azure-pipelines\win32\listprocesses.bat displayName: Diagnostics before smoke test run diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 3c92499b2a6..7bd034d960a 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -89,20 +89,38 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication + # Remove once kerberos 2.1.2-alpha.0 or newer is available. + # Ref https://github.com/mongodb-js/kerberos/pull/187 which contains the following patch. + - pwsh: | + $includes = @' + { + 'target_defaults': { + 'conditions': [ + ['OS=="win"', { + 'msvs_configuration_attributes': { + 'SpectreMitigation': 'Spectre' + } + }] + ] + } + } + '@ + + if (!(Test-Path "~/.gyp")) { + mkdir "~/.gyp" + } + echo $includes > "~/.gyp/include.gypi" + displayName: Create include.gypi + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - powershell: | . build/azure-pipelines/win32/exec.ps1 . build/azure-pipelines/win32/retry.ps1 $ErrorActionPreference = "Stop" - # TODO: remove custom node-gyp when updating to Node v20, - # refs https://github.com/npm/cli/releases/tag/v10.2.3 which is available with Node >= 20.10.0 - $nodeGypDir = "$(Agent.TempDirectory)/custom-packages" - mkdir "$nodeGypDir" - npm install node-gyp@10.0.1 -g --prefix "$nodeGypDir" - $env:npm_config_node_gyp = "${nodeGypDir}/node_modules/node-gyp/bin/node-gyp.js" - $env:npm_config_arch = "$(VSCODE_ARCH)" - $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile --check-files } } env: + npm_config_arch: $(VSCODE_ARCH) + CHILD_CONCURRENCY: 1 ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -166,7 +184,6 @@ steps: env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -177,7 +194,6 @@ steps: env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - template: product-build-win32-test.yml@self @@ -318,7 +334,7 @@ steps: sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH) sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Server" sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], ''), ne(variables['VSCODE_ARCH'], 'arm64')) + condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) displayName: Publish server archive - task: 1ES.PublishPipelineArtifact@1 @@ -328,7 +344,7 @@ steps: sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)-web sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Web" sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], ''), ne(variables['VSCODE_ARCH'], 'arm64')) + condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) displayName: Publish web server archive - task: 1ES.PublishPipelineArtifact@1 diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 86f78d0adea..52950abd800 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -69b40637a88ad4c17877b3d665b39ad0e11928aa71b19ef45f5b76250d1c9786 *chromedriver-v28.2.8-darwin-arm64.zip -3a9ce6179228245f2c7878c4238e10d51c77dc20642922a226ccc235a20f5a29 *chromedriver-v28.2.8-darwin-x64.zip -7f6470ea5d86dbe68fcc3fccfefd3b7135ba3468ef54b0235bf57cedeabf433d *chromedriver-v28.2.8-linux-arm64.zip -4bfe709d58b237f5c5a7618b2abecf533dac9415d327e763ad6cf622218517cc *chromedriver-v28.2.8-linux-armv7l.zip -7558ee413f96f88b9b9ad5787dd433adcfaf56411fdf052826d39d204ebaba9d *chromedriver-v28.2.8-linux-x64.zip -9814583b075d969c32afb6e929b4bf7956b0223fded996c91341388b8f638dd6 *chromedriver-v28.2.8-mas-arm64.zip -82d11c6606db9aea355b1e410083c72bd1e39abb9e34a839c16b16b75364ea0d *chromedriver-v28.2.8-mas-x64.zip -4803a5335a40ba208136094f5adfde2c4272761d34e0e9e9f4febc2ef676c3ad *chromedriver-v28.2.8-win32-arm64.zip -7b079f47869f7e96a5829f6fb7eff032394f76218b39a2aaf73cc93ce8a68050 *chromedriver-v28.2.8-win32-ia32.zip -2aedd176d4f72b29cd1914364e813756d52f53558df32e3429996b820edc994d *chromedriver-v28.2.8-win32-x64.zip -ae1a521aa36053a3b60b318d7bc093ec7579af6aa8b02bffe1f9e70d6922b726 *electron-api.json -a916f0cc438258f42f43955157565e7eca14966266f3fb123c8c736bece97daa *electron-v28.2.8-darwin-arm64-dsym-snapshot.zip -3c31d0a105b0632f15aa8adc68f06dc8ca47b1fdf1e62d1436ac43af117a22fb *electron-v28.2.8-darwin-arm64-dsym.zip -dab03f1cd7b499552d503bcca2fc1c3f40a1d2c463655ca3ace20778f08e9b04 *electron-v28.2.8-darwin-arm64-symbols.zip -2965d8c8d64fb6c51f5a283a246de653bfae22fe4bf9adf6c04592afabf62f04 *electron-v28.2.8-darwin-arm64.zip -03511a34d94d27eb576ab20e3a432c082a32a298475c7a85a329e029dddc55e4 *electron-v28.2.8-darwin-x64-dsym-snapshot.zip -96089786bd2723786673561c9b6f9a154928de663f2411f10153e6c985703eef *electron-v28.2.8-darwin-x64-dsym.zip -872789c3c218ab8f98be83c7781e3e6ef0114bd39780d65eaae77e99dbbda1de *electron-v28.2.8-darwin-x64-symbols.zip -a7889addd37254f842798bdd3ca34752b75acf6d8dd456cdeb2d75590c0a9ceb *electron-v28.2.8-darwin-x64.zip -fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-arm64-debug.zip -591248f7c94a6d7c4a4d8b2fcf63c8e4347018a65e1f68ed90e5549a587062c8 *electron-v28.2.8-linux-arm64-symbols.zip -6183db1029cebd9e0fb0e4f2d24a80b0274c5265756e66cb9fa0a480b92c98ea *electron-v28.2.8-linux-arm64.zip -fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-armv7l-debug.zip -87c4c534cd1d447b9d4632585a0d79c9d31114bd39ca63df1f2384afae3aa6b7 *electron-v28.2.8-linux-armv7l-symbols.zip -2a772b65815a0d47a756eed52f76cd9f27a8c277d7998bfcfe93b84a346eb255 *electron-v28.2.8-linux-armv7l.zip -773aa1f0bbe2b79765bf498958565f63957f8ec2e42327978a143dcbbc7f1bea *electron-v28.2.8-linux-x64-debug.zip -f8cbc6f2b719cc2f623afcfde8cb1d42614708793621a7a97b328015366b9b8f *electron-v28.2.8-linux-x64-symbols.zip -e7d17ee311299dfef3d2916987a513c4c1b66ad2e417c15fa5d29699602bd6cb *electron-v28.2.8-linux-x64.zip -5f0179fd7bf3927381bde24c9fb372fe95328be0500918cd6ee7f9503fae1ef5 *electron-v28.2.8-mas-arm64-dsym-snapshot.zip -e9810019f1d7b1b5a93fd1aee8adda5a872ebfb170de6d55cdd55162b923432d *electron-v28.2.8-mas-arm64-dsym.zip -4781376244c7df89d119575e2788ad43fae4387d850ef672665688081b30997c *electron-v28.2.8-mas-arm64-symbols.zip -a3932199781970e0b2fdb805d6556287ca877b35ac19384da00474140e14c41f *electron-v28.2.8-mas-arm64.zip -326cde32079496e0d976c5b65e85e5ce208eea3d8d23cd92c9e25f0fa6b30f40 *electron-v28.2.8-mas-x64-dsym-snapshot.zip -59a2b3d28dba45ee3016f8ab49a71b0c55f99ef046476183bc36890c9d335a71 *electron-v28.2.8-mas-x64-dsym.zip -313ff88f568c39079a1b7a1011f77fa03890cb9bb53649a489643311303cc3b8 *electron-v28.2.8-mas-x64-symbols.zip -41ab9f3addea5066d7e0ace28ebaead7128a2073931473c847aa9133b7df9248 *electron-v28.2.8-mas-x64.zip -179de6dd4835216bcd3e8bb9eb4d4b54013df865f52dbf0d5214726fc31cba9a *electron-v28.2.8-win32-arm64-pdb.zip -8628dec571206001420c1d8655904883d5de7e772d51ab2101b002c22e0dd25c *electron-v28.2.8-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-arm64-toolchain-profile.zip -bb2a2a466d14c32c06ff09c42b3d1413f19fdc8a49a445d07d289fa453c268d3 *electron-v28.2.8-win32-arm64.zip -1d1efc3a1d17072bc76a4a63c8236a896d46f6f3badacd50bc5824149196d56f *electron-v28.2.8-win32-ia32-pdb.zip -9ddb1520de421a7c636160d01432c9bf111e6ef4b9a3be41b185c702c72353ac *electron-v28.2.8-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-ia32-toolchain-profile.zip -38e22f9b0a32e0fc26e81905214e244c0a5d5c19e13c8ca2329ac75b62881472 *electron-v28.2.8-win32-ia32.zip -8168296e0454377e0113a7d0f87535d3d0e0c1a8538e8079ee1aae9c7223bb02 *electron-v28.2.8-win32-x64-pdb.zip -a276e9e748fa7db970e7dcce6f4ae571d8615a44e5208c0fa3c03de08774a4aa *electron-v28.2.8-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-x64-toolchain-profile.zip -079cc98f7933992ac7154e21e160d4a4c6b3541c26b56fc6f8438e9eabc369b9 *electron-v28.2.8-win32-x64.zip -f838e4a7c24518c5fa25d4a23acf869737cfa88761019cea4f83ebfb302363ec *electron.d.ts -4450bcc66cece4ff2373563e0123799f95645fa155577a8f380211b29e8b4ec9 *ffmpeg-v28.2.8-darwin-arm64.zip -152e3ed53098d24f356d7ec640d19efc57f7f34c39d8b8278f2586985d4a99a1 *ffmpeg-v28.2.8-darwin-x64.zip -8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.8-linux-arm64.zip -51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.8-linux-armv7l.zip -acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.8-linux-x64.zip -15a2a4a28a66e65122eb4f2bd796ccd5b6ed45420a034878affd002fc8c290dc *ffmpeg-v28.2.8-mas-arm64.zip -2dfe2f524c5220f50c7b6fe08605a67631b5520e0c82842e1f41f677cac17643 *ffmpeg-v28.2.8-mas-x64.zip -313e2979f0df88715159c0737bfbb5ae1d5c79fb9820e94d2a93ba71d3324ecd *ffmpeg-v28.2.8-win32-arm64.zip -9e73bc07563aefa8b9625676939a410b35a823d961b96da0e8edd90d7e5fb47b *ffmpeg-v28.2.8-win32-ia32.zip -1b11042defc8a3f403e5567fa4a4b8c59b224f3b7b52d44d6c7197b96af7b53b *ffmpeg-v28.2.8-win32-x64.zip -1e2e9480d4228f6bbc731ff7ee413b9e97656c36b15418d20681a76d82902b86 *hunspell_dictionaries.zip -8c8b967cf4c78ed9bbf4921b2c616257f45b137412eb3bc64176066c3e47bbe8 *libcxx-objects-v28.2.8-linux-arm64.zip -56af259535ccfaac295b82ce68686f9582265cb2ebe2783852f518c0fabc8a1e *libcxx-objects-v28.2.8-linux-armv7l.zip -b590e001dc98e32e5952ca69573e6f1bcec5e2f2d99052d1089ab72084cccea1 *libcxx-objects-v28.2.8-linux-x64.zip -c0634d5c92f0a2983b17c866f7d3694cb75f6e78cd07b10d9488ef46acc66a50 *libcxx_headers.zip -99ee16441d9eb2b92a05d5a5c9b9dc4cdfab33cb09595e9d78fd2ba503dead5b *libcxxabi_headers.zip -a95de1da301d641caaafaea9869c4c7834c254f818ac0c10d97402b2220c8be3 *mksnapshot-v28.2.8-darwin-arm64.zip -e5ef6b35d7cd807f93babfedbbde513ab6053ad9fb80b0f7abc1bfda414daaa1 *mksnapshot-v28.2.8-darwin-x64.zip -eeb6c5b7962af8d5cfaa97b2cf96d312d0ad57a3abb3e00774d50ea2e005bb9b *mksnapshot-v28.2.8-linux-arm64-x64.zip -0adacd0767469f90400b1f17ba8ac3ccb33cfeb11a8ef54d70bc8adb7cc306dc *mksnapshot-v28.2.8-linux-armv7l-x64.zip -5242817f1f26e10804e7e2446d0a8a64e8b2958cdba01e79d89db883d9d960d0 *mksnapshot-v28.2.8-linux-x64.zip -0ecb67673508c10f4fe08e7cb80300b9a8f507f50994c79caf302ff78ef748ca *mksnapshot-v28.2.8-mas-arm64.zip -19429da56077f12de4d4563f49c55f4f1f0fe61f66863804640fc55e65ee98f9 *mksnapshot-v28.2.8-mas-x64.zip -c7b47ae63c2f6eb07b06379206e6f215fbcb2b9a49faa72ca850bf8f9b998c4c *mksnapshot-v28.2.8-win32-arm64-x64.zip -0032660a9f8575a153951f29adae49a18e400b40906eec803fe7e3d2e970503d *mksnapshot-v28.2.8-win32-ia32.zip -2c71c9a2bd4441e580dc3083073e712fba94e0236415c8ab35320da52f492508 *mksnapshot-v28.2.8-win32-x64.zip +cdf0522dacc5fdf75a9a4ca9a20f049793ef8bae2b04e37f02e8923fcecd4c76 *chromedriver-v30.1.2-darwin-arm64.zip +f739afd34c48aae18da1d9dffb2332f0c2b2e27ff2056ac619e0a8c9414618f0 *chromedriver-v30.1.2-darwin-x64.zip +c2a220a316c268984bb8135975f28adecc392cf5cd2244af8cc21d60018a6a10 *chromedriver-v30.1.2-linux-arm64.zip +fad358f076caff4eecc3d8b63cea2e109cc0ff8b4632bb4edd21a7c7721bc428 *chromedriver-v30.1.2-linux-armv7l.zip +addc230541e9ee44b311fba9e900c5cb7d8b4d64b79a6d5dae68a71ced1c4611 *chromedriver-v30.1.2-linux-x64.zip +e2d0876fac8af41e0dd9c1b14c5315b426c55217e54e2b4ca5e28faae1b19557 *chromedriver-v30.1.2-mas-arm64.zip +c705d0b74a4658c197a87ed1e9e2509e55186769376b40493ec68b7cbb36c312 *chromedriver-v30.1.2-mas-x64.zip +8bbf9ccb789b236dec3e871d2499c14926a4c2dd3c865f8c7316ba3aa5b3f58f *chromedriver-v30.1.2-win32-arm64.zip +75af4b07bbd3a8fd7e18d63eb936e11054d01b52d438e4f7b7c5e6b82a41dec3 *chromedriver-v30.1.2-win32-ia32.zip +2d306df2c66314be12df78b6139ebf2d616463efdf4017473330d87a61954c3a *chromedriver-v30.1.2-win32-x64.zip +1990fe83ca25b990773ce099b90fb804fc026c1398279731229ba37d02c23000 *electron-api.json +64360b0db764c1bf16a8ab810a25c03f68873691d2897b360281bc50a645b6bb *electron-v30.1.2-darwin-arm64-dsym-snapshot.zip +3192307419ee2bafc3c99c62d79dbd2e6cba5815ae245be994f0b1e1d7fedb46 *electron-v30.1.2-darwin-arm64-dsym.zip +823eadc46a498af7433dae2ace1c8f0b2b0c8cbf98204fed0fdce8110ddafbc1 *electron-v30.1.2-darwin-arm64-symbols.zip +3c651624b1605411e595a6e9f6e874effb947c80eda4b8d0bb7d2972ff6ff242 *electron-v30.1.2-darwin-arm64.zip +fa5cbbc3e7760907229fa0753c3faa2b43e09bcb987b6c3f693e0030aa65e62b *electron-v30.1.2-darwin-x64-dsym-snapshot.zip +d97087f4c7e41fd67b2f0f7aa623ccf1effdbb94133b883dbae1e4c42a576b03 *electron-v30.1.2-darwin-x64-dsym.zip +a129109ed6ca23f66d625ebff9e57be117bc0a32c4f4348e78c1ad7dd41c4189 *electron-v30.1.2-darwin-x64-symbols.zip +8645e10af9b047c765a6cc880f9fa53f266e618569eaf65c0ea9fa1058be20f7 *electron-v30.1.2-darwin-x64.zip +afa016399f57bbbb658238dd715ef2a66790602ff46514e9cd99f2e078789c7d *electron-v30.1.2-linux-arm64-debug.zip +aec05a3e46a83d7c3e502b04245d3d6d2db8d5789a4dcc991f7cf6e7e3cd7036 *electron-v30.1.2-linux-arm64-symbols.zip +953c51413abfd62efdba070f99c961202ce5ef5e77c5cee5eaaf097ac2f5bc9b *electron-v30.1.2-linux-arm64.zip +afa016399f57bbbb658238dd715ef2a66790602ff46514e9cd99f2e078789c7d *electron-v30.1.2-linux-armv7l-debug.zip +04a6851e218c9a6f70870a341beea1b194a94d2d78f3a283065a34654cce7e30 *electron-v30.1.2-linux-armv7l-symbols.zip +677d6b4e6721ffb27b0037628c235cd0a9f10104beabb2b6c67d4b0328a9d001 *electron-v30.1.2-linux-armv7l.zip +1b6d8926e2c7cacbb33e56259ebe908c52e2a6dbbe37f0def043121f228a3a37 *electron-v30.1.2-linux-x64-debug.zip +dc4b526b02ec028d20836ee4617335d0569170846959761c3e8dc615d668f596 *electron-v30.1.2-linux-x64-symbols.zip +724aeca4b2f428f544e9b7e5e52e2074458c2e198f588530819cd0318af8599d *electron-v30.1.2-linux-x64.zip +15b10a6cf6a1ea029792282088647abbf58b415fdb4dd004c2c67c4a8f216ef3 *electron-v30.1.2-mas-arm64-dsym-snapshot.zip +00dbfc36c8ff6ecfbb8b01b51fee41f876bf3456017a8cc694a0c56608061f5e *electron-v30.1.2-mas-arm64-dsym.zip +e402b68cad20ee60cce376dacf0e2e72f1fcd0b6359dda7789626dca4b101e8b *electron-v30.1.2-mas-arm64-symbols.zip +8e59fc7c6df96a029310d6f7769e0c76592dc746d31764b35460de129231d12b *electron-v30.1.2-mas-arm64.zip +e8bfe6b58d2767dd52a7668df380be9c786abed0b25d642bee70c278758d2e77 *electron-v30.1.2-mas-x64-dsym-snapshot.zip +418f32558f9a107ebc942666a6ca680874db8ed438ae3f0064255abe0f9ce77e *electron-v30.1.2-mas-x64-dsym.zip +6d05da37cb39c664a764c879216e762efdb66f97734e42bbaa8f115b11fd3c87 *electron-v30.1.2-mas-x64-symbols.zip +af7b85a28593227add7e595e01d570e19512b40a29599ec007ef7cd4b5a11435 *electron-v30.1.2-mas-x64.zip +580c9b9fee6bacfdc4d3a1118953ba0096bcc19d28b1d804d72d40c5caac8d81 *electron-v30.1.2-win32-arm64-pdb.zip +dbb0ce79933c3688b7fa2bf04ce083d6e8da0a8c07b5104f53805e2e92679cd4 *electron-v30.1.2-win32-arm64-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.1.2-win32-arm64-toolchain-profile.zip +1b9035b541999e0cedcfe3893bb3c1497435494279d599c4e9300fe204f4d560 *electron-v30.1.2-win32-arm64.zip +3cb6869a69d118488dc48ed573b3fc9445bea33cf0a04338ca1b8000b4eaf516 *electron-v30.1.2-win32-ia32-pdb.zip +3f353849ef21506d5ca7a2c26df84d7c744ef1795acd33307f501764dfbe9bc1 *electron-v30.1.2-win32-ia32-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.1.2-win32-ia32-toolchain-profile.zip +431aff46eca9ddb726af3c8d6f4bbb72158e5c872c1b8bee221b3df0a8e94947 *electron-v30.1.2-win32-ia32.zip +85428715d302d4c97e47ca0b6409497191846baccbc36debf895cde724f9445f *electron-v30.1.2-win32-x64-pdb.zip +a5156829bc0caab5c70e6cd352941b6fb7b1e396d7869298c1ceaa69d742e3dc *electron-v30.1.2-win32-x64-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.1.2-win32-x64-toolchain-profile.zip +d400ed1aca2b7b1093aee8b5a8544b112e9f81a570cf77f6bdfe019ceb003f7f *electron-v30.1.2-win32-x64.zip +3187cdd642b968e17a768a3fdaff44bedb69954be3d88e7ad55aac9787b70485 *electron.d.ts +4c8bd4215102f563cf0464ae4edd4022921c53e0ebd4bb6b6f7f7434d50081aa *ffmpeg-v30.1.2-darwin-arm64.zip +2b8c57755ccb64a64540433d3cfbfecd29d07e28bf23c5f08a2edd0e2333a645 *ffmpeg-v30.1.2-darwin-x64.zip +c6f69859dc2ca04daa7c218936e5bc044fc19582423db954c5e8664ce7f331df *ffmpeg-v30.1.2-linux-arm64.zip +12f760ce312e4ee98ddd9496600ca9a1468e28f0ed6d41c6b9284dec841f550f *ffmpeg-v30.1.2-linux-armv7l.zip +c7207cc057849033d550d6897a53f7abee455f31b8f571c3c57a01f2871ff5af *ffmpeg-v30.1.2-linux-x64.zip +447b19465677636261ce47cc256f221c660e66cd136b33c3c742b8d23481b924 *ffmpeg-v30.1.2-mas-arm64.zip +e44fcdb5d0e62d328c0bb5ebeed54f388b09591a33e4dc1698c421e4c9d881dc *ffmpeg-v30.1.2-mas-x64.zip +b470cc217c4f06b3fe4edf6695b4762e7c926d07c1f41c3cab0ddb1d71ce8ce4 *ffmpeg-v30.1.2-win32-arm64.zip +92f1743c16210a77d07e9553cf96ccfb4e6985cc50ee831b5b075589aa0ebb05 *ffmpeg-v30.1.2-win32-ia32.zip +b2fa79b739023651f0551ca06ac5da7b47acaaf8b09ccdedf7081fc3ea824a80 *ffmpeg-v30.1.2-win32-x64.zip +b2b562a45ae4a2d40bf039ed1c707a7875b9e893fc8c6a0044d536b0f9968629 *hunspell_dictionaries.zip +08da936356d1321eec550c30a9208750773d86e3be0b7fe4baf36c72a8609c20 *libcxx-objects-v30.1.2-linux-arm64.zip +8aa302ac17f6ac44f756cb9219f18d01d267d43f9af2dfd8d4626e9d01e584fa *libcxx-objects-v30.1.2-linux-armv7l.zip +1ab04d6cec407930a2051761e6114cb2cd6418e2103d32e835246d06c696b427 *libcxx-objects-v30.1.2-linux-x64.zip +4db17e017bdb818cca5d8e08a78fe54fb907c3cf05defd1b8f086b413075357a *libcxx_headers.zip +209f20bd3ee59fa7c85b0789d8e45168583407c9fb5bd2eba446c1e0796c4a7b *libcxxabi_headers.zip +4dcc59b7c66b9ccc881dbdfeba9de707f693fd839a8e764d34bb66dec7e3e63c *mksnapshot-v30.1.2-darwin-arm64.zip +582886552cbc227eb71f5047d00ac62baeb1912a52a8f5132953b94f54d41dd4 *mksnapshot-v30.1.2-darwin-x64.zip +639fde4957353d48be54b9075c0894b7529210f0bb3a2f9cad81ba2deb415080 *mksnapshot-v30.1.2-linux-arm64-x64.zip +1fec2ae49a9f03117a5e5b637866e5aa270da2fa3a007d0314c8a39ed392230f *mksnapshot-v30.1.2-linux-armv7l-x64.zip +494f86a178a2b14101c6deccf7c2ae88c690c7ce8445b63854a2e0525d69aaa0 *mksnapshot-v30.1.2-linux-x64.zip +bfbb90138b4df3f57fd9fe9cc05b6b8f9b3bf099b66d65215cc9434e2272b0d1 *mksnapshot-v30.1.2-mas-arm64.zip +d1a6a825628a141fdd73cf208180cc20af32b8e06aeb7f4d36566358cef40a90 *mksnapshot-v30.1.2-mas-x64.zip +fd523788a380990d589f9461f29a8878b63090146aa124f2debf3197af69929b *mksnapshot-v30.1.2-win32-arm64-x64.zip +69068d132cd511e33be9aff6dda5df74079c2003657cd89107d4eaaac7e4d997 *mksnapshot-v30.1.2-win32-ia32.zip +69bfab7461f83a256c0869e2781cca4f5a53f8b6a60d78617b79b8bfc0b554b6 *mksnapshot-v30.1.2-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 13aa4c7e87b..877d8afe243 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,6 +1,7 @@ -9f982cc91b28778dd8638e4f94563b0c2a1da7aba62beb72bd427721035ab553 node-v18.18.2-darwin-arm64.tar.gz -5bb8da908ed590e256a69bf2862238c8a67bc4600119f2f7721ca18a7c810c0f node-v18.18.2-darwin-x64.tar.gz -0c9a6502b66310cb26e12615b57304e91d92ac03d4adcb91c1906351d7928f0d node-v18.18.2-linux-arm64.tar.gz -7a3b34a6fdb9514bc2374114ec6df3c36113dc5075c38b22763aa8f106783737 node-v18.18.2-linux-armv7l.tar.gz -a44c3e7f8bf91e852c928e5d8bd67ca316b35e27eec1d8acbe3b9dbe03688dab node-v18.18.2-linux-x64.tar.gz -54884183ff5108874c091746465e8156ae0acc68af589cc10bc41b3927db0f4a win-x64/node.exe +d2148d79e9ff04d2982d00faeae942ceba488ca327a91065e528235167b9e9d6 node-v20.14.0-darwin-arm64.tar.gz +1dcc18a199cb5f46d43ed1c3c61b87a247d1a1a11dd6b32a36a9c46ac1088f86 node-v20.14.0-darwin-x64.tar.gz +d63e83fca4f81801396620c46a42892a2ef26e21a4508f68de373e61a12bd9c5 node-v20.14.0-linux-arm64.tar.gz +af45ea0d09e55a4f05c0190636532bdf9f70b2eaf0a1c4d7594207cf21284df0 node-v20.14.0-linux-armv7l.tar.gz +5b9bf40cfc7c21de14a1b4c367650e3c96eb101156bf9368ffc2f947414b6581 node-v20.14.0-linux-x64.tar.gz +a6ec02119098cf92592539e06289953c4365be20ab15d4ad264669f931000b0c win-arm64/node.exe +8f45741ec6ba07be8d199c0cebc838a58c0430c9228dbe50f8ac5d4859e58bae win-x64/node.exe diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 7da8e55c908..a3daf1878b0 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); const fs = require("fs"); +const minimatch = require("minimatch"); const vscode_universal_bundler_1 = require("vscode-universal-bundler"); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); const root = path.dirname(path.dirname(__dirname)); @@ -18,26 +19,29 @@ async function main(buildDir) { const appName = product.nameLong + '.app'; const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); - const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const filesToSkip = [ + '**/CodeResources', + '**/Credits.rtf', + ]; await (0, vscode_universal_bundler_1.makeUniversalApp)({ x64AppPath, arm64AppPath, - x64AsarPath, - arm64AsarPath, - filesToSkip: [ - 'product.json', - 'Credits.rtf', - 'CodeResources', - 'fsevents.node', - 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds - 'MainMenu.nib', // Generated sequence is not deterministic with Xcode 13 - '.npmrc' - ], + asarPath: asarRelativePath, outAppPath, - force: true + force: true, + mergeASARs: true, + x64ArchFiles: '*/kerberos.node', + filesToSkipComparison: (file) => { + for (const expected of filesToSkip) { + if (minimatch(file, expected)) { + return true; + } + } + return false; + } }); const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); Object.assign(productJson, { diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index ffba8952cd8..94b8a23b9e5 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import * as fs from 'fs'; +import * as minimatch from 'minimatch'; import { makeUniversalApp } from 'vscode-universal-bundler'; import { spawn } from '@malept/cross-spawn-promise'; @@ -21,27 +22,31 @@ async function main(buildDir?: string) { const appName = product.nameLong + '.app'; const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); - const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const filesToSkip = [ + '**/CodeResources', + '**/Credits.rtf', + ]; + await makeUniversalApp({ x64AppPath, arm64AppPath, - x64AsarPath, - arm64AsarPath, - filesToSkip: [ - 'product.json', - 'Credits.rtf', - 'CodeResources', - 'fsevents.node', - 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds - 'MainMenu.nib', // Generated sequence is not deterministic with Xcode 13 - '.npmrc' - ], + asarPath: asarRelativePath, outAppPath, - force: true + force: true, + mergeASARs: true, + x64ArchFiles: '*/kerberos.node', + filesToSkipComparison: (file: string) => { + for (const expected of filesToSkip) { + if (minimatch(file, expected)) { + return true; + } + } + return false; + } }); const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); diff --git a/build/filters.js b/build/filters.js index 0e0c5fcabb8..e4d74a6cfb3 100644 --- a/build/filters.js +++ b/build/filters.js @@ -81,6 +81,7 @@ module.exports.indentationFilter = [ '!resources/linux/snap/electron-launch', '!build/ext.js', '!build/npm/gyp/patches/gyp_spectre_mitigation_support.patch', + '!product.overrides.json', // except specific folders '!test/automation/out/**', @@ -116,7 +117,7 @@ module.exports.indentationFilter = [ '!src/vs/*/**/*.d.ts', '!src/typings/**/*.d.ts', '!extensions/**/*.d.ts', - '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', + '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,psm1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', '!build/{lib,download,linux,darwin}/**/*.js', '!build/**/*.sh', '!build/azure-pipelines/**/*.js', @@ -198,7 +199,7 @@ module.exports.eslintFilter = [ .toString().split(/\r\n|\n/) .filter(line => !line.startsWith('#')) .filter(line => !!line) - .map(line => `!${line}`) + .map(line => line.startsWith('!') ? line.slice(1) : `!${line}`) ]; module.exports.stylelintFilter = [ diff --git a/build/gulpfile.cli.js b/build/gulpfile.cli.js index 86646fdb274..592fc74516c 100644 --- a/build/gulpfile.cli.js +++ b/build/gulpfile.cli.js @@ -5,8 +5,6 @@ 'use strict'; -//@ts-check - const es = require('event-stream'); const gulp = require('gulp'); const path = require('path'); @@ -24,7 +22,6 @@ const createReporter = require('./lib/reporter').createReporter; const root = 'cli'; const rootAbs = path.resolve(__dirname, '..', root); const src = `${root}/src`; -const targetCliPath = path.join(root, 'target', 'debug', process.platform === 'win32' ? 'code.exe' : 'code'); const platformOpensslDirName = process.platform === 'win32' ? ( diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js index c4947e76cbf..de8f3c4fb57 100644 --- a/build/gulpfile.compile.js +++ b/build/gulpfile.compile.js @@ -3,18 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check 'use strict'; const gulp = require('gulp'); const util = require('./lib/util'); +const date = require('./lib/date'); const task = require('./lib/task'); const compilation = require('./lib/compilation'); const optimize = require('./lib/optimize'); +/** + * @param {boolean} disableMangle + */ function makeCompileBuildTask(disableMangle) { return task.series( util.rimraf('out-build'), util.buildWebNodePaths('out-build'), + date.writeISODate('out-build'), compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out-build', true, { disableMangle }), optimize.optimizeLoaderTask('out-build', 'out-build', true) diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 22b70a953df..fe29d4fe183 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -29,19 +29,17 @@ const editorEntryPoints = [ { name: 'vs/editor/editor.main', include: [], - exclude: ['vs/css', 'vs/nls'], + exclude: ['vs/css'], prepend: [ - { path: 'out-editor-build/vs/css.js', amdModuleId: 'vs/css' }, - { path: 'out-editor-build/vs/nls.js', amdModuleId: 'vs/nls' } + { path: 'out-editor-build/vs/css.js', amdModuleId: 'vs/css' } ], }, { name: 'vs/base/common/worker/simpleWorker', include: ['vs/editor/common/services/editorSimpleWorker'], - exclude: ['vs/nls'], + exclude: [], prepend: [ { path: 'vs/loader.js' }, - { path: 'vs/nls.js', amdModuleId: 'vs/nls' }, { path: 'vs/base/worker/workerMain.js' } ], dest: 'vs/base/worker/workerMain.js' @@ -86,7 +84,8 @@ const extractEditorSrcTask = task.define('extract-editor-src', () => { }); // Disable mangling for the editor, as it complicates debugging & quite a few users rely on private/protected fields. -const compileEditorAMDTask = task.define('compile-editor-amd', compilation.compileTask('out-editor-src', 'out-editor-build', true, { disableMangle: true })); +// Disable NLS task to remove english strings to preserve backwards compatibility when we removed the `vs/nls!` AMD plugin. +const compileEditorAMDTask = task.define('compile-editor-amd', compilation.compileTask('out-editor-src', 'out-editor-build', true, { disableMangle: true, preserveEnglish: true })); const optimizeEditorAMDTask = task.define('optimize-editor-amd', optimize.optimizeTask( { @@ -99,7 +98,6 @@ const optimizeEditorAMDTask = task.define('optimize-editor-amd', optimize.optimi paths: { 'vs': 'out-editor-build/vs', 'vs/css': 'out-editor-build/vs/css.build', - 'vs/nls': 'out-editor-build/vs/nls.build', 'vscode': 'empty:' } }, @@ -124,7 +122,6 @@ const createESMSourcesAndResourcesTask = task.define('extract-editor-esm', () => 'vs/base/worker/workerMain.ts', ], renames: { - 'vs/nls.mock.ts': 'vs/nls.ts' } }); }); diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 32a66abfbc2..4631b295ae4 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -48,7 +48,6 @@ const compilations = [ 'extensions/json-language-features/client/tsconfig.json', 'extensions/json-language-features/server/tsconfig.json', 'extensions/markdown-language-features/preview-src/tsconfig.json', - 'extensions/markdown-language-features/server/tsconfig.json', 'extensions/markdown-language-features/tsconfig.json', 'extensions/markdown-math/tsconfig.json', 'extensions/media-preview/tsconfig.json', @@ -66,10 +65,12 @@ const compilations = [ 'extensions/typescript-language-features/tsconfig.json', 'extensions/vscode-api-tests/tsconfig.json', 'extensions/vscode-colorize-tests/tsconfig.json', - 'extensions/vscode-test-resolver/tsconfig.json' + 'extensions/vscode-test-resolver/tsconfig.json', + + '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', ]; -const getBaseUrl = out => `https://ticino.blob.core.windows.net/sourcemaps/${commit}/${out}`; +const getBaseUrl = out => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`; const tasks = compilations.map(function (tsconfigFile) { const absolutePath = path.join(root, tsconfigFile); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index c2b81d0cf7c..560bdc1f6b7 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -12,11 +12,13 @@ const util = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); const optimize = require('./lib/optimize'); +const { inlineMeta } = require('./lib/inlineMeta'); const product = require('../product.json'); const rename = require('gulp-rename'); const replace = require('gulp-replace'); const filter = require('gulp-filter'); const { getProductionDependencies } = require('./lib/dependencies'); +const { readISODate } = require('./lib/date'); const vfs = require('vinyl-fs'); const packageJson = require('../package.json'); const flatmap = require('gulp-flatmap'); @@ -39,6 +41,7 @@ const REMOTE_FOLDER = path.join(REPO_ROOT, 'remote'); const BUILD_TARGETS = [ { platform: 'win32', arch: 'x64' }, + { platform: 'win32', arch: 'arm64' }, { platform: 'darwin', arch: 'x64' }, { platform: 'darwin', arch: 'arm64' }, { platform: 'linux', arch: 'x64' }, @@ -52,14 +55,8 @@ const BUILD_TARGETS = [ const serverResources = [ - // Bootstrap - 'out-build/bootstrap.js', - 'out-build/bootstrap-fork.js', - 'out-build/bootstrap-amd.js', - 'out-build/bootstrap-node.js', - - // Performance - 'out-build/vs/base/common/performance.js', + // NLS + 'out-build/nls.messages.json', // Process monitor 'out-build/vs/base/node/cpuUsage.sh', @@ -67,6 +64,8 @@ const serverResources = [ // Terminal shell integration 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1', + 'out-build/vs/workbench/contrib/terminal/browser/media/CodeTabExpansion.psm1', + 'out-build/vs/workbench/contrib/terminal/browser/media/GitTabExpansion.psm1', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh', @@ -89,23 +88,23 @@ const serverWithWebResources = [ const serverEntryPoints = [ { name: 'vs/server/node/server.main', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/server/node/server.cli', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/workbench/api/node/extensionHostProcess', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/platform/files/node/watcher/watcherMain', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/platform/terminal/node/ptyHostMain', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] } ]; @@ -118,6 +117,12 @@ const serverWithWebEntryPoints = [ ...vscodeWebEntryPoints ]; +const commonJSEntryPoints = [ + 'out-build/server-main.js', + 'out-build/server-cli.js', + 'out-build/bootstrap-fork.js', +]; + function getNodeVersion() { const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const nodeVersion = /^target "(.*)"$/m.exec(yarnrc)[1]; @@ -125,20 +130,7 @@ function getNodeVersion() { return { nodeVersion, internalNodeVersion }; } -function getNodeChecksum(nodeVersion, platform, arch, glibcPrefix) { - let expectedName; - switch (platform) { - case 'win32': - expectedName = `win-${arch}/node.exe`; - break; - - case 'darwin': - case 'alpine': - case 'linux': - expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; - break; - } - +function getNodeChecksum(expectedName) { const nodeJsChecksums = fs.readFileSync(path.join(REPO_ROOT, 'build', 'checksums', 'nodejs.txt'), 'utf8'); for (const line of nodeJsChecksums.split('\n')) { const [checksum, name] = line.split(/\s+/); @@ -182,7 +174,6 @@ if (defaultNodeTask) { function nodejs(platform, arch) { const { fetchUrls, fetchGithub } = require('./lib/fetch'); const untar = require('gulp-untar'); - const crypto = require('crypto'); if (arch === 'armhf') { arch = 'armv7l'; @@ -194,7 +185,24 @@ function nodejs(platform, arch) { log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from ${product.nodejsRepository}...`); const glibcPrefix = process.env['VSCODE_NODE_GLIBC'] ?? ''; - const checksumSha256 = getNodeChecksum(nodeVersion, platform, arch, glibcPrefix); + let expectedName; + switch (platform) { + case 'win32': + expectedName = product.nodejsRepository !== 'https://nodejs.org' ? + `win-${arch}-node.exe` : `win-${arch}/node.exe`; + break; + + case 'darwin': + expectedName = `node-v${nodeVersion}-${platform}-${arch}.tar.gz`; + break; + case 'linux': + expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; + break; + case 'alpine': + expectedName = `node-v${nodeVersion}-linux-${arch}-musl.tar.gz`; + break; + } + const checksumSha256 = getNodeChecksum(expectedName); if (checksumSha256) { log(`Using SHA256 checksum for checking integrity: ${checksumSha256}`); @@ -205,13 +213,13 @@ function nodejs(platform, arch) { switch (platform) { case 'win32': return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `win-${arch}-node.exe`, checksumSha256 }) : + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 })) .pipe(rename('node.exe')); case 'darwin': case 'linux': return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`, checksumSha256 }) : + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 }) ).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) @@ -219,7 +227,7 @@ function nodejs(platform, arch) { .pipe(rename('node')); case 'alpine': return product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `node-v${nodeVersion}-${platform}-${arch}.tar.gz`, checksumSha256 }) + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) .pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) .pipe(util.setExecutableBit('**')) @@ -288,13 +296,22 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa } const name = product.nameShort; + + let packageJsonContents; const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) - .pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined })); - - const date = new Date().toISOString(); + .pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined })) + .pipe(es.through(function (file) { + packageJsonContents = file.contents.toString(); + this.emit('data', file); + })); + let productJsonContents; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date, version })); + .pipe(json({ commit, date: readISODate('out-build'), version })) + .pipe(es.through(function (file) { + productJsonContents = file.contents.toString(); + this.emit('data', file); + })); const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); @@ -387,6 +404,12 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa ); } + result = inlineMeta(result, { + targetPaths: commonJSEntryPoints, + packageJsonFn: () => packageJsonContents, + productJsonFn: () => productJsonContents + }); + return result.pipe(vfs.dest(destination)); }; } @@ -418,16 +441,14 @@ function tweakProductForServerWeb(product) { }, commonJS: { src: 'out-build', - entryPoints: [ - 'out-build/server-main.js', - 'out-build/server-cli.js' - ], + entryPoints: commonJSEntryPoints, platform: 'node', external: [ 'minimist', - // TODO: we cannot inline `product.json` because + // We cannot inline `product.json` from here because // it is being changed during build time at a later // point in time (such as `checksums`) + // We have a manual step to inline these later. '../product.json', '../package.json' ] @@ -439,7 +460,7 @@ function tweakProductForServerWeb(product) { const minifyTask = task.define(`minify-vscode-${type}`, task.series( optimizeTask, util.rimraf(`out-vscode-${type}-min`), - optimize.minifyTask(`out-vscode-${type}`, `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask(`out-vscode-${type}`, `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) )); gulp.task(minifyTask); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index e1507e0424f..edca10c8420 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -6,10 +6,7 @@ 'use strict'; const gulp = require('gulp'); -const merge = require('gulp-merge-json'); const fs = require('fs'); -const os = require('os'); -const cp = require('child_process'); const path = require('path'); const es = require('event-stream'); const vfs = require('vinyl-fs'); @@ -18,9 +15,11 @@ const replace = require('gulp-replace'); const filter = require('gulp-filter'); const util = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); +const { readISODate } = require('./lib/date'); const task = require('./lib/task'); const buildfile = require('../src/buildfile'); const optimize = require('./lib/optimize'); +const { inlineMeta } = require('./lib/inlineMeta'); const root = path.dirname(__dirname); const commit = getVersion(root); const packageJson = require('../package.json'); @@ -51,16 +50,12 @@ const vscodeEntryPoints = [ ].flat(); const vscodeResources = [ - 'out-build/bootstrap.js', - 'out-build/bootstrap-fork.js', - 'out-build/bootstrap-amd.js', - 'out-build/bootstrap-node.js', - 'out-build/bootstrap-window.js', + 'out-build/nls.messages.json', + 'out-build/nls.keys.json', 'out-build/vs/**/*.{svg,png,html,jpg,mp3}', '!out-build/vs/code/browser/**/*.html', '!out-build/vs/code/**/*-dev.html', '!out-build/vs/editor/standalone/**/*.svg', - 'out-build/vs/base/common/performance.js', 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh,ps.sh}', 'out-build/vs/base/browser/ui/codicons/codicon/**', 'out-build/vs/base/parts/sandbox/electron-sandbox/preload.js', @@ -70,9 +65,12 @@ const vscodeResources = [ 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', 'out-build/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/*.fish', 'out-build/vs/workbench/contrib/terminal/browser/media/*.ps1', + 'out-build/vs/workbench/contrib/terminal/browser/media/*.psm1', 'out-build/vs/workbench/contrib/terminal/browser/media/*.sh', 'out-build/vs/workbench/contrib/terminal/browser/media/*.zsh', 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', + '!out-build/vs/workbench/contrib/issue/browser/*.html', + '!out-build/vs/workbench/contrib/issue/**/*-dev.html', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/contrib/tasks/**/*.json', '!**/test/**' @@ -82,11 +80,16 @@ const vscodeResources = [ // be inlined into the target window file in this order // and they depend on each other in this way. const windowBootstrapFiles = [ - 'out-build/bootstrap.js', 'out-build/vs/loader.js', 'out-build/bootstrap-window.js' ]; +const commonJSEntryPoints = [ + 'out-build/main.js', + 'out-build/cli.js', + 'out-build/bootstrap-fork.js' +]; + const optimizeVSCodeTask = task.define('optimize-vscode', task.series( util.rimraf('out-vscode'), // Optimize: bundles source files automatically based on @@ -105,24 +108,24 @@ const optimizeVSCodeTask = task.define('optimize-vscode', task.series( }, commonJS: { src: 'out-build', - entryPoints: [ - 'out-build/main.js', - 'out-build/cli.js' - ], + entryPoints: commonJSEntryPoints, platform: 'node', external: [ 'electron', 'minimist', - // TODO: we cannot inline `product.json` because + 'original-fs', + // We cannot inline `product.json` from here because // it is being changed during build time at a later // point in time (such as `checksums`) + // We have a manual step to inline these later. '../product.json', '../package.json', ] }, manual: [ { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/workbench/workbench.js'], out: 'vs/code/electron-sandbox/workbench/workbench.js' }, - { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/issue/issueReporter.js'], out: 'vs/code/electron-sandbox/issue/issueReporter.js' }, + // TODO: @justchen https://github.com/microsoft/vscode/issues/213332 make sure to remove when we use window.open on desktop. + { src: [...windowBootstrapFiles, 'out-build/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js'], out: 'vs/workbench/contrib/issue/electron-sandbox/issueReporter.js' }, { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js'], out: 'vs/code/electron-sandbox/processExplorer/processExplorer.js' } ] } @@ -130,7 +133,7 @@ const optimizeVSCodeTask = task.define('optimize-vscode', task.series( )); gulp.task(optimizeVSCodeTask); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const minifyVSCodeTask = task.define('minify-vscode', task.series( optimizeVSCodeTask, util.rimraf('out-vscode-min'), @@ -246,14 +249,21 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op packageJsonUpdates.desktopName = `${product.applicationName}-url-handler.desktop`; } + let packageJsonContents; const packageJsonStream = gulp.src(['package.json'], { base: '.' }) - .pipe(json(packageJsonUpdates)); - - const date = new Date().toISOString(); - const productJsonUpdate = { commit, date, checksums, version }; + .pipe(json(packageJsonUpdates)) + .pipe(es.through(function (file) { + packageJsonContents = file.contents.toString(); + this.emit('data', file); + })); + let productJsonContents; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json(productJsonUpdate)); + .pipe(json({ commit, date: readISODate('out-build'), checksums, version })) + .pipe(es.through(function (file) { + productJsonContents = file.contents.toString(); + this.emit('data', file); + })); const license = gulp.src([product.licenseFileName, 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.', allowEmpty: true }); @@ -278,10 +288,13 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op '**/*.node', '**/@vscode/ripgrep/bin/*', '**/node-pty/build/Release/*', + '**/node-pty/build/Release/conpty/*', '**/node-pty/lib/worker/conoutSocketWorker.js', '**/node-pty/lib/shared/conout.js', '**/*.wasm', '**/@vscode/vsce-sign/bin/*', + ], [ + '**/*.mk', ], 'node_modules.asar')); let all = es.merge( @@ -386,6 +399,12 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(rename('bin/' + product.applicationName))); } + result = inlineMeta(result, { + targetPaths: commonJSEntryPoints, + packageJsonFn: () => packageJsonContents, + productJsonFn: () => productJsonContents + }); + return result.pipe(vfs.dest(destination)); }; } @@ -493,17 +512,12 @@ gulp.task(task.define( core, compileExtensionsBuildTask, function () { - const pathToMetadata = './out-vscode/nls.metadata.json'; - const pathToRehWebMetadata = './out-vscode-reh-web/nls.metadata.json'; + const pathToMetadata = './out-build/nls.metadata.json'; const pathToExtensions = '.build/extensions/*'; const pathToSetup = 'build/win32/i18n/messages.en.isl'; return es.merge( - gulp.src([pathToMetadata, pathToRehWebMetadata]).pipe(merge({ - fileName: 'nls.metadata.json', - jsonSpace: '', - concatArrays: true - })).pipe(i18n.createXlfFilesForCoreBundle()), + gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) ).pipe(vfs.dest('../vscode-translations-export')); diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 8c2b62f7b2a..79594543c3b 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -8,10 +8,9 @@ const gulp = require('gulp'); const replace = require('gulp-replace'); const rename = require('gulp-rename'); -const shell = require('gulp-shell'); const es = require('event-stream'); const vfs = require('vinyl-fs'); -const util = require('./lib/util'); +const { rimraf } = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); const packageJson = require('../package.json'); @@ -19,6 +18,10 @@ const product = require('../product.json'); const dependenciesGenerator = require('./linux/dependencies-generator'); const debianRecommendedDependencies = require('./linux/debian/dep-lists').recommendedDeps; const path = require('path'); +const cp = require('child_process'); +const util = require('util'); + +const exec = util.promisify(cp.exec); const root = path.dirname(__dirname); const commit = getVersion(root); @@ -105,7 +108,11 @@ function prepareDebPackage(arch) { .pipe(replace('@@NAME@@', product.applicationName)) .pipe(rename('DEBIAN/postinst')); - const all = es.merge(control, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); + const templates = gulp.src('resources/linux/debian/templates.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('DEBIAN/templates')); + + const all = es.merge(control, templates, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); return all.pipe(vfs.dest(destination)); }; @@ -116,11 +123,13 @@ function prepareDebPackage(arch) { */ function buildDebPackage(arch) { const debArch = getDebPackageArch(arch); - return shell.task([ - 'chmod 755 ' + product.applicationName + '-' + debArch + '/DEBIAN/postinst ' + product.applicationName + '-' + debArch + '/DEBIAN/prerm ' + product.applicationName + '-' + debArch + '/DEBIAN/postrm', - 'mkdir -p deb', - 'fakeroot dpkg-deb -b ' + product.applicationName + '-' + debArch + ' deb' - ], { cwd: '.build/linux/deb/' + debArch }); + const cwd = `.build/linux/deb/${debArch}`; + + return async () => { + await exec(`chmod 755 ${product.applicationName}-${debArch}/DEBIAN/postinst ${product.applicationName}-${debArch}/DEBIAN/prerm ${product.applicationName}-${debArch}/DEBIAN/postrm`, { cwd }); + await exec('mkdir -p deb', { cwd }); + await exec(`fakeroot dpkg-deb -b ${product.applicationName}-${debArch} deb`, { cwd }); + }; } /** @@ -218,14 +227,14 @@ function prepareRpmPackage(arch) { function buildRpmPackage(arch) { const rpmArch = getRpmPackageArch(arch); const rpmBuildPath = getRpmBuildPath(rpmArch); - const rpmOut = rpmBuildPath + '/RPMS/' + rpmArch; - const destination = '.build/linux/rpm/' + rpmArch; + const rpmOut = `${rpmBuildPath}/RPMS/${rpmArch}`; + const destination = `.build/linux/rpm/${rpmArch}`; - return shell.task([ - 'mkdir -p ' + destination, - 'HOME="$(pwd)/' + destination + '" rpmbuild -bb ' + rpmBuildPath + '/SPECS/' + product.applicationName + '.spec --target=' + rpmArch, - 'cp "' + rpmOut + '/$(ls ' + rpmOut + ')" ' + destination + '/' - ]); + return async () => { + await exec(`mkdir -p ${destination}`); + await exec(`HOME="$(pwd)/${destination}" rpmbuild -bb ${rpmBuildPath}/SPECS/${product.applicationName}.spec --target=${rpmArch}`); + await exec(`cp "${rpmOut}/$(ls ${rpmOut})" ${destination}/`); + }; } /** @@ -286,9 +295,8 @@ function prepareSnapPackage(arch) { * @param {string} arch */ function buildSnapPackage(arch) { - const snapBuildPath = getSnapBuildPath(arch); - // Default target for snapcraft runs: pull, build, stage and prime, and finally assembles the snap. - return shell.task(`cd ${snapBuildPath} && snapcraft`); + const cwd = getSnapBuildPath(arch); + return () => exec('snapcraft', { cwd }); } const BUILD_TARGETS = [ @@ -299,18 +307,18 @@ const BUILD_TARGETS = [ BUILD_TARGETS.forEach(({ arch }) => { const debArch = getDebPackageArch(arch); - const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(util.rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); + const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); gulp.task(prepareDebTask); const buildDebTask = task.define(`vscode-linux-${arch}-build-deb`, buildDebPackage(arch)); gulp.task(buildDebTask); const rpmArch = getRpmPackageArch(arch); - const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(util.rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); + const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); gulp.task(prepareRpmTask); const buildRpmTask = task.define(`vscode-linux-${arch}-build-rpm`, buildRpmPackage(arch)); gulp.task(buildRpmTask); - const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(util.rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); + const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); gulp.task(prepareSnapTask); const buildSnapTask = task.define(`vscode-linux-${arch}-build-snap`, task.series(prepareSnapTask, buildSnapPackage(arch))); gulp.task(buildSnapTask); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 85129a523da..0424b12fa45 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -12,12 +12,12 @@ const util = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); const optimize = require('./lib/optimize'); +const { readISODate } = require('./lib/date'); const product = require('../product.json'); const rename = require('gulp-rename'); const filter = require('gulp-filter'); const { getProductionDependencies } = require('./lib/dependencies'); const vfs = require('vinyl-fs'); -const replace = require('gulp-replace'); const packageJson = require('../package.json'); const { compileBuildTask } = require('./gulpfile.compile'); const extensions = require('./lib/extensions'); @@ -31,12 +31,16 @@ const quality = product.quality; const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; const vscodeWebResourceIncludes = [ + // Workbench 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg,mp3}', 'out-build/vs/code/browser/workbench/*.html', 'out-build/vs/base/browser/ui/codicons/codicon/**/*.ttf', 'out-build/vs/**/markdown.css', + // NLS + 'out-build/nls.messages.js', + // Webview 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', 'out-build/vs/workbench/contrib/webview/browser/pre/*.html', @@ -70,14 +74,11 @@ const vscodeWebEntryPoints = [ buildfile.workerNotebook, buildfile.workerLanguageDetection, buildfile.workerLocalFileSearch, - buildfile.workerProfileAnalysis, buildfile.keyboardMaps, buildfile.workbenchWeb ].flat(); exports.vscodeWebEntryPoints = vscodeWebEntryPoints; -const buildDate = new Date().toISOString(); - /** * @param {object} product The parsed product.json file contents */ @@ -93,7 +94,7 @@ const createVSCodeWebProductConfigurationPatcher = (product) => { ...product, version, commit, - date: buildDate + date: readISODate('out-build') }); return content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfiguration.substr(1, productConfiguration.length - 2) /* without { and }*/); } @@ -175,7 +176,7 @@ const optimizeVSCodeWebTask = task.define('optimize-vscode-web', task.series( const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( optimizeVSCodeWebTask, util.rimraf('out-vscode-web-min'), - optimize.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask('out-vscode-web', `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) )); gulp.task(minifyVSCodeWebTask); diff --git a/build/hygiene.js b/build/hygiene.js index 3809036dad1..bc64a11f8e9 100644 --- a/build/hygiene.js +++ b/build/hygiene.js @@ -23,7 +23,7 @@ const copyrightHeaderLines = [ function hygiene(some, linting = true) { const gulpeslint = require('gulp-eslint'); const gulpstylelint = require('./stylelint'); - const tsfmt = require('typescript-formatter'); + const formatter = require('./lib/formatter'); let errorCount = 0; @@ -111,38 +111,23 @@ function hygiene(some, linting = true) { }); const formatting = es.map(function (file, cb) { - tsfmt - .processString(file.path, file.contents.toString('utf8'), { - verify: false, - tsfmt: true, - // verbose: true, - // keep checkJS happy - editorconfig: undefined, - replace: undefined, - tsconfig: undefined, - tsconfigFile: undefined, - tsfmtFile: undefined, - vscode: undefined, - vscodeFile: undefined, - }) - .then( - (result) => { - const original = result.src.replace(/\r\n/gm, '\n'); - const formatted = result.dest.replace(/\r\n/gm, '\n'); + try { + const rawInput = file.contents.toString('utf8'); + const rawOutput = formatter.format(file.path, rawInput); - if (original !== formatted) { - console.error( - `File not formatted. Run the 'Format Document' command to fix it:`, - file.relative - ); - errorCount++; - } - cb(null, file); - }, - (err) => { - cb(err); - } - ); + const original = rawInput.replace(/\r\n/gm, '\n'); + const formatted = rawOutput.replace(/\r\n/gm, '\n'); + if (original !== formatted) { + console.error( + `File not formatted. Run the 'Format Document' command to fix it:`, + file.relative + ); + errorCount++; + } + cb(null, file); + } catch (err) { + cb(err); + } }); let input; @@ -256,7 +241,7 @@ function createGitIndexVinyls(paths) { cp.exec( process.platform === 'win32' ? `git show :${relativePath}` : `git show ':${relativePath}'`, - { maxBuffer: 2000 * 1024, encoding: 'buffer' }, + { maxBuffer: stat.size, encoding: 'buffer' }, (err, out) => { if (err) { return e(err); diff --git a/build/lib/asar.js b/build/lib/asar.js index 31845f2f2dd..07b39bf79ff 100644 --- a/build/lib/asar.js +++ b/build/lib/asar.js @@ -11,7 +11,7 @@ const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); const VinylFile = require("vinyl"); const minimatch = require("minimatch"); -function createAsar(folderPath, unpackGlobs, destFilename) { +function createAsar(folderPath, unpackGlobs, skipGlobs, destFilename) { const shouldUnpackFile = (file) => { for (let i = 0; i < unpackGlobs.length; i++) { if (minimatch(file.relative, unpackGlobs[i])) { @@ -20,6 +20,14 @@ function createAsar(folderPath, unpackGlobs, destFilename) { } return false; }; + const shouldSkipFile = (file) => { + for (const skipGlob of skipGlobs) { + if (minimatch(file.relative, skipGlob)) { + return true; + } + } + return false; + }; const filesystem = new Filesystem(folderPath); const out = []; // Keep track of pending inserts @@ -64,6 +72,9 @@ function createAsar(folderPath, unpackGlobs, destFilename) { if (!file.stat.isFile()) { throw new Error(`unknown item in stream!`); } + if (shouldSkipFile(file)) { + return; + } const shouldUnpack = shouldUnpackFile(file); insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); if (shouldUnpack) { diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 44a6416bdfb..7dc1dd3b2e6 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -17,7 +17,7 @@ declare class AsarFilesystem { insertFile(path: string, shouldUnpack: boolean, file: { stat: { size: number; mode: number } }, options: {}): Promise; } -export function createAsar(folderPath: string, unpackGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { +export function createAsar(folderPath: string, unpackGlobs: string[], skipGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { const shouldUnpackFile = (file: VinylFile): boolean => { for (let i = 0; i < unpackGlobs.length; i++) { @@ -28,6 +28,15 @@ export function createAsar(folderPath: string, unpackGlobs: string[], destFilena return false; }; + const shouldSkipFile = (file: VinylFile): boolean => { + for (const skipGlob of skipGlobs) { + if (minimatch(file.relative, skipGlob)) { + return true; + } + } + return false; + }; + const filesystem = new Filesystem(folderPath); const out: Buffer[] = []; @@ -78,6 +87,9 @@ export function createAsar(folderPath: string, unpackGlobs: string[], destFilena if (!file.stat.isFile()) { throw new Error(`unknown item in stream!`); } + if (shouldSkipFile(file)) { + return; + } const shouldUnpack = shouldUnpackFile(file); insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 61d9f015624..df7c10fa37e 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -5,6 +5,7 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.bundle = bundle; +exports.removeDuplicateTSBoilerplate = removeDuplicateTSBoilerplate; const fs = require("fs"); const path = require("path"); const vm = require("vm"); @@ -36,14 +37,10 @@ function bundle(entryPoints, config, callback) { const loader = loaderModule.exports; config.isBuild = true; config.paths = config.paths || {}; - if (!config.paths['vs/nls']) { - config.paths['vs/nls'] = 'out-build/vs/nls.build'; - } if (!config.paths['vs/css']) { config.paths['vs/css'] = 'out-build/vs/css.build'; } config.buildForceInvokeFactory = config.buildForceInvokeFactory || {}; - config.buildForceInvokeFactory['vs/nls'] = true; config.buildForceInvokeFactory['vs/css'] = true; loader.config(config); loader(['require'], (localRequire) => { @@ -53,7 +50,6 @@ function bundle(entryPoints, config, callback) { r += '.js'; } // avoid packaging the build version of plugins: - r = r.replace('vs/nls.build.js', 'vs/nls.js'); r = r.replace('vs/css.build.js', 'vs/css.js'); return { path: r, amdModuleId: entry.amdModuleId }; }; @@ -132,7 +128,7 @@ function emitEntryPoints(modules, entryPoints) { }); return { // TODO@TS 2.1.2 - files: extractStrings(removeDuplicateTSBoilerplate(result)), + files: extractStrings(removeAllDuplicateTSBoilerplate(result)), bundleData: bundleData }; } @@ -221,7 +217,16 @@ function extractStrings(destFiles) { }); return destFiles; } -function removeDuplicateTSBoilerplate(destFiles) { +function removeAllDuplicateTSBoilerplate(destFiles) { + destFiles.forEach((destFile) => { + const SEEN_BOILERPLATE = []; + destFile.sources.forEach((source) => { + source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); + }); + }); + return destFiles; +} +function removeDuplicateTSBoilerplate(source, SEEN_BOILERPLATE = []) { // Taken from typescript compiler => emitFiles const BOILERPLATE = [ { start: /^var __extends/, end: /^}\)\(\);$/ }, @@ -231,46 +236,43 @@ function removeDuplicateTSBoilerplate(destFiles) { { start: /^var __param/, end: /^};$/ }, { start: /^var __awaiter/, end: /^};$/ }, { start: /^var __generator/, end: /^};$/ }, + { start: /^var __createBinding/, end: /^}\)\);$/ }, + { start: /^var __setModuleDefault/, end: /^}\);$/ }, + { start: /^var __importStar/, end: /^};$/ }, ]; - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE = []; - destFile.sources.forEach((source) => { - const lines = source.contents.split(/\r\n|\n|\r/); - const newLines = []; - let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - if (END_BOILERPLATE.test(line)) { - IS_REMOVING_BOILERPLATE = false; - } - } - else { - for (let j = 0; j < BOILERPLATE.length; j++) { - const boilerplate = BOILERPLATE[j]; - if (boilerplate.start.test(line)) { - if (SEEN_BOILERPLATE[j]) { - IS_REMOVING_BOILERPLATE = true; - END_BOILERPLATE = boilerplate.end; - } - else { - SEEN_BOILERPLATE[j] = true; - } - } - } - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); + const lines = source.split(/\r\n|\n|\r/); + const newLines = []; + let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + if (END_BOILERPLATE.test(line)) { + IS_REMOVING_BOILERPLATE = false; + } + } + else { + for (let j = 0; j < BOILERPLATE.length; j++) { + const boilerplate = BOILERPLATE[j]; + if (boilerplate.start.test(line)) { + if (SEEN_BOILERPLATE[j]) { + IS_REMOVING_BOILERPLATE = true; + END_BOILERPLATE = boilerplate.end; } else { - newLines.push(line); + SEEN_BOILERPLATE[j] = true; } } } - source.contents = newLines.join('\n'); - }); - }); - return destFiles; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + } + else { + newLines.push(line); + } + } + } + return newLines.join('\n'); } function emitEntryPoint(modulesMap, deps, entryPoint, includedModules, prepend, append, dest) { if (!dest) { diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index c5fdc2da18c..74d37fcce55 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -138,14 +138,10 @@ export function bundle(entryPoints: IEntryPoint[], config: ILoaderConfig, callba const loader: any = loaderModule.exports; config.isBuild = true; config.paths = config.paths || {}; - if (!config.paths['vs/nls']) { - config.paths['vs/nls'] = 'out-build/vs/nls.build'; - } if (!config.paths['vs/css']) { config.paths['vs/css'] = 'out-build/vs/css.build'; } config.buildForceInvokeFactory = config.buildForceInvokeFactory || {}; - config.buildForceInvokeFactory['vs/nls'] = true; config.buildForceInvokeFactory['vs/css'] = true; loader.config(config); @@ -156,7 +152,6 @@ export function bundle(entryPoints: IEntryPoint[], config: ILoaderConfig, callba r += '.js'; } // avoid packaging the build version of plugins: - r = r.replace('vs/nls.build.js', 'vs/nls.js'); r = r.replace('vs/css.build.js', 'vs/css.js'); return { path: r, amdModuleId: entry.amdModuleId }; }; @@ -256,7 +251,7 @@ function emitEntryPoints(modules: IBuildModuleInfo[], entryPoints: IEntryPointMa return { // TODO@TS 2.1.2 - files: extractStrings(removeDuplicateTSBoilerplate(result)), + files: extractStrings(removeAllDuplicateTSBoilerplate(result)), bundleData: bundleData }; } @@ -355,7 +350,19 @@ function extractStrings(destFiles: IConcatFile[]): IConcatFile[] { return destFiles; } -function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { +function removeAllDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { + destFiles.forEach((destFile) => { + const SEEN_BOILERPLATE: boolean[] = []; + destFile.sources.forEach((source) => { + source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); + }); + }); + + return destFiles; +} + +export function removeDuplicateTSBoilerplate(source: string, SEEN_BOILERPLATE: boolean[] = []): string { + // Taken from typescript compiler => emitFiles const BOILERPLATE = [ { start: /^var __extends/, end: /^}\)\(\);$/ }, @@ -365,46 +372,42 @@ function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { { start: /^var __param/, end: /^};$/ }, { start: /^var __awaiter/, end: /^};$/ }, { start: /^var __generator/, end: /^};$/ }, + { start: /^var __createBinding/, end: /^}\)\);$/ }, + { start: /^var __setModuleDefault/, end: /^}\);$/ }, + { start: /^var __importStar/, end: /^};$/ }, ]; - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE: boolean[] = []; - destFile.sources.forEach((source) => { - const lines = source.contents.split(/\r\n|\n|\r/); - const newLines: string[] = []; - let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE: RegExp; + const lines = source.split(/\r\n|\n|\r/); + const newLines: string[] = []; + let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE: RegExp; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - if (END_BOILERPLATE!.test(line)) { - IS_REMOVING_BOILERPLATE = false; - } - } else { - for (let j = 0; j < BOILERPLATE.length; j++) { - const boilerplate = BOILERPLATE[j]; - if (boilerplate.start.test(line)) { - if (SEEN_BOILERPLATE[j]) { - IS_REMOVING_BOILERPLATE = true; - END_BOILERPLATE = boilerplate.end; - } else { - SEEN_BOILERPLATE[j] = true; - } - } - } - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + if (END_BOILERPLATE!.test(line)) { + IS_REMOVING_BOILERPLATE = false; + } + } else { + for (let j = 0; j < BOILERPLATE.length; j++) { + const boilerplate = BOILERPLATE[j]; + if (boilerplate.start.test(line)) { + if (SEEN_BOILERPLATE[j]) { + IS_REMOVING_BOILERPLATE = true; + END_BOILERPLATE = boilerplate.end; } else { - newLines.push(line); + SEEN_BOILERPLATE[j] = true; } } } - source.contents = newLines.join('\n'); - }); - }); - - return destFiles; + if (IS_REMOVING_BOILERPLATE) { + newLines.push(''); + } else { + newLines.push(line); + } + } + } + return newLines.join('\n'); } interface IPluginMap { diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 85cd722dbf3..cafca34a0d8 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -23,6 +23,7 @@ const ts = require("typescript"); const File = require("vinyl"); const task = require("./task"); const index_1 = require("./mangle/index"); +const postcss_1 = require("./postcss"); const watch = require('./watch'); // --- gulp-tsb: compile and transpile -------------------------------- const reporter = (0, reporter_1.createReporter)(); @@ -40,7 +41,7 @@ function getTypeScriptCompilerOptions(src) { options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 0 : 1; return options; } -function createCompile(src, build, emitError, transpileOnly) { +function createCompile(src, { build, emitError, transpileOnly, preserveEnglish }) { const tsb = require('./tsb'); const sourcemaps = require('gulp-sourcemaps'); const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); @@ -60,18 +61,17 @@ function createCompile(src, build, emitError, transpileOnly) { const isRuntimeJs = (f) => f.path.endsWith('.js') && !f.path.includes('fixtures'); const isCSS = (f) => f.path.endsWith('.css') && !f.path.includes('fixtures'); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); - const postcss = require('gulp-postcss'); const postcssNesting = require('postcss-nesting'); const input = es.through(); const output = input .pipe(util.$if(isUtf8Test, bom())) // this is required to preserve BOM in test files that loose it otherwise .pipe(util.$if(!build && isRuntimeJs, util.appendOwnPathSourceURL())) - .pipe(util.$if(isCSS, postcss([postcssNesting()]))) + .pipe(util.$if(isCSS, (0, postcss_1.gulpPostcss)([postcssNesting()], err => reporter(String(err))))) .pipe(tsFilter) .pipe(util.loadSourcemaps()) .pipe(compilation(token)) .pipe(noDeclarationsFilter) - .pipe(util.$if(build, nls.nls())) + .pipe(util.$if(build, nls.nls({ preserveEnglish }))) .pipe(noDeclarationsFilter.restore) .pipe(util.$if(!transpileOnly, sourcemaps.write('.', { addComment: false, @@ -90,7 +90,7 @@ function createCompile(src, build, emitError, transpileOnly) { } function transpileTask(src, out, swc) { const task = () => { - const transpile = createCompile(src, false, true, { swc }); + const transpile = createCompile(src, { build: false, emitError: true, transpileOnly: { swc }, preserveEnglish: false }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); return srcPipe .pipe(transpile()) @@ -104,7 +104,7 @@ function compileTask(src, out, build, options = {}) { if (os.totalmem() < 4_000_000_000) { throw new Error('compilation requires 4GB of RAM'); } - const compile = createCompile(src, build, true, false); + const compile = createCompile(src, { build, emitError: true, transpileOnly: false, preserveEnglish: !!options.preserveEnglish }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); const generator = new MonacoGenerator(false); if (src === 'src') { @@ -141,7 +141,7 @@ function compileTask(src, out, build, options = {}) { } function watchTask(out, build) { const task = () => { - const compile = createCompile('src', build, false, false); + const compile = createCompile('src', { build, emitError: false, transpileOnly: false, preserveEnglish: false }); const src = gulp.src('src/**', { base: 'src' }); const watchSrc = watch('src/**', { base: 'src', readDelay: 200 }); const generator = new MonacoGenerator(true); @@ -234,7 +234,7 @@ class MonacoGenerator { function generateApiProposalNames() { let eol; try { - const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); eol = match ? match[0] : os.EOL; } @@ -242,18 +242,27 @@ function generateApiProposalNames() { eol = os.EOL; } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; - const proposalNames = new Set(); + const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; + const proposals = new Map(); const input = es.through(); const output = input .pipe(util.filter((f) => pattern.test(f.path))) .pipe(es.through((f) => { const name = path.basename(f.path); const match = pattern.exec(name); - if (match) { - proposalNames.add(match[1]); + if (!match) { + return; } + const proposalName = match[1]; + const contents = f.contents.toString('utf8'); + const versionMatch = versionPattern.exec(contents); + const version = versionMatch ? versionMatch[1] : undefined; + proposals.set(proposalName, { + proposal: `https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${proposalName}.d.ts`, + version: version ? parseInt(version) : undefined + }); }, function () { - const names = [...proposalNames.values()].sort(); + const names = [...proposals.keys()].sort(); const contents = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -262,14 +271,18 @@ function generateApiProposalNames() { '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '', - 'export const allApiProposals = Object.freeze({', - `${names.map(name => `\t${name}: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${name}.d.ts'`).join(`,${eol}`)}`, - '});', - 'export type ApiProposalName = keyof typeof allApiProposals;', + 'const _allApiProposals = {', + `${names.map(proposalName => { + const proposal = proposals.get(proposalName); + return `\t${proposalName}: {${eol}\t\tproposal: '${proposal.proposal}',${eol}${proposal.version ? `\t\tversion: ${proposal.version}${eol}` : ''}\t}`; + }).join(`,${eol}`)}`, + '};', + 'export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals);', + 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); this.emit('data', new File({ - path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', + path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 94bfe6e832d..8c3614b4c13 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -19,6 +19,7 @@ import * as File from 'vinyl'; import * as task from './task'; import { Mangler } from './mangle/index'; import { RawSourceMap } from 'source-map'; +import { gulpPostcss } from './postcss'; const watch = require('./watch'); @@ -41,7 +42,14 @@ function getTypeScriptCompilerOptions(src: string): ts.CompilerOptions { return options; } -function createCompile(src: string, build: boolean, emitError: boolean, transpileOnly: boolean | { swc: boolean }) { +interface ICompileTaskOptions { + readonly build: boolean; + readonly emitError: boolean; + readonly transpileOnly: boolean | { swc: boolean }; + readonly preserveEnglish: boolean; +} + +function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish }: ICompileTaskOptions) { const tsb = require('./tsb') as typeof import('./tsb'); const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); @@ -67,19 +75,18 @@ function createCompile(src: string, build: boolean, emitError: boolean, transpil const isCSS = (f: File) => f.path.endsWith('.css') && !f.path.includes('fixtures'); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); - const postcss = require('gulp-postcss') as typeof import('gulp-postcss'); const postcssNesting = require('postcss-nesting'); const input = es.through(); const output = input .pipe(util.$if(isUtf8Test, bom())) // this is required to preserve BOM in test files that loose it otherwise .pipe(util.$if(!build && isRuntimeJs, util.appendOwnPathSourceURL())) - .pipe(util.$if(isCSS, postcss([postcssNesting()]))) + .pipe(util.$if(isCSS, gulpPostcss([postcssNesting()], err => reporter(String(err))))) .pipe(tsFilter) .pipe(util.loadSourcemaps()) .pipe(compilation(token)) .pipe(noDeclarationsFilter) - .pipe(util.$if(build, nls.nls())) + .pipe(util.$if(build, nls.nls({ preserveEnglish }))) .pipe(noDeclarationsFilter.restore) .pipe(util.$if(!transpileOnly, sourcemaps.write('.', { addComment: false, @@ -102,7 +109,7 @@ export function transpileTask(src: string, out: string, swc: boolean): task.Stre const task = () => { - const transpile = createCompile(src, false, true, { swc }); + const transpile = createCompile(src, { build: false, emitError: true, transpileOnly: { swc }, preserveEnglish: false }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); return srcPipe @@ -114,7 +121,7 @@ export function transpileTask(src: string, out: string, swc: boolean): task.Stre return task; } -export function compileTask(src: string, out: string, build: boolean, options: { disableMangle?: boolean } = {}): task.StreamTask { +export function compileTask(src: string, out: string, build: boolean, options: { disableMangle?: boolean; preserveEnglish?: boolean } = {}): task.StreamTask { const task = () => { @@ -122,7 +129,7 @@ export function compileTask(src: string, out: string, build: boolean, options: { throw new Error('compilation requires 4GB of RAM'); } - const compile = createCompile(src, build, true, false); + const compile = createCompile(src, { build, emitError: true, transpileOnly: false, preserveEnglish: !!options.preserveEnglish }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); const generator = new MonacoGenerator(false); if (src === 'src') { @@ -166,7 +173,7 @@ export function compileTask(src: string, out: string, build: boolean, options: { export function watchTask(out: string, build: boolean): task.StreamTask { const task = () => { - const compile = createCompile('src', build, false, false); + const compile = createCompile('src', { build, emitError: false, transpileOnly: false, preserveEnglish: false }); const src = gulp.src('src/**', { base: 'src' }); const watchSrc = watch('src/**', { base: 'src', readDelay: 200 }); @@ -275,7 +282,7 @@ function generateApiProposalNames() { let eol: string; try { - const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); eol = match ? match[0] : os.EOL; } catch { @@ -283,7 +290,8 @@ function generateApiProposalNames() { } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; - const proposalNames = new Set(); + const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; + const proposals = new Map(); const input = es.through(); const output = input @@ -292,11 +300,22 @@ function generateApiProposalNames() { const name = path.basename(f.path); const match = pattern.exec(name); - if (match) { - proposalNames.add(match[1]); + if (!match) { + return; } + + const proposalName = match[1]; + + const contents = f.contents.toString('utf8'); + const versionMatch = versionPattern.exec(contents); + const version = versionMatch ? versionMatch[1] : undefined; + + proposals.set(proposalName, { + proposal: `https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${proposalName}.d.ts`, + version: version ? parseInt(version) : undefined + }); }, function () { - const names = [...proposalNames.values()].sort(); + const names = [...proposals.keys()].sort(); const contents = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -305,15 +324,19 @@ function generateApiProposalNames() { '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '', - 'export const allApiProposals = Object.freeze({', - `${names.map(name => `\t${name}: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${name}.d.ts'`).join(`,${eol}`)}`, - '});', - 'export type ApiProposalName = keyof typeof allApiProposals;', + 'const _allApiProposals = {', + `${names.map(proposalName => { + const proposal = proposals.get(proposalName)!; + return `\t${proposalName}: {${eol}\t\tproposal: '${proposal.proposal}',${eol}${proposal.version ? `\t\tversion: ${proposal.version}${eol}` : ''}\t}`; + }).join(`,${eol}`)}`, + '};', + 'export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals);', + 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); this.emit('data', new File({ - path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', + path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); diff --git a/build/lib/date.js b/build/lib/date.js new file mode 100644 index 00000000000..77fff0e5073 --- /dev/null +++ b/build/lib/date.js @@ -0,0 +1,32 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.writeISODate = writeISODate; +exports.readISODate = readISODate; +const path = require("path"); +const fs = require("fs"); +const root = path.join(__dirname, '..', '..'); +/** + * Writes a `outDir/date` file with the contents of the build + * so that other tasks during the build process can use it and + * all use the same date. + */ +function writeISODate(outDir) { + const result = () => new Promise((resolve, _) => { + const outDirectory = path.join(root, outDir); + fs.mkdirSync(outDirectory, { recursive: true }); + const date = new Date().toISOString(); + fs.writeFileSync(path.join(outDirectory, 'date'), date, 'utf8'); + resolve(); + }); + result.taskName = 'build-date-file'; + return result; +} +function readISODate(outDir) { + const outDirectory = path.join(root, outDir); + return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); +} +//# sourceMappingURL=date.js.map \ No newline at end of file diff --git a/build/lib/date.ts b/build/lib/date.ts new file mode 100644 index 00000000000..998e89f8e6a --- /dev/null +++ b/build/lib/date.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as fs from 'fs'; + +const root = path.join(__dirname, '..', '..'); + +/** + * Writes a `outDir/date` file with the contents of the build + * so that other tasks during the build process can use it and + * all use the same date. + */ +export function writeISODate(outDir: string) { + const result = () => new Promise((resolve, _) => { + const outDirectory = path.join(root, outDir); + fs.mkdirSync(outDirectory, { recursive: true }); + + const date = new Date().toISOString(); + fs.writeFileSync(path.join(outDirectory, 'date'), date, 'utf8'); + + resolve(); + }); + result.taskName = 'build-date-file'; + return result; +} + +export function readISODate(outDir: string): string { + const outDirectory = path.join(root, outDir); + return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); +} diff --git a/build/lib/electron.js b/build/lib/electron.js index 8524b18850c..99252e4e64a 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) { role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } diff --git a/build/lib/electron.ts b/build/lib/electron.ts index ba93c3a2af3..7a2a2a19557 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 6a6c0a7b4cd..7bc9cb51cae 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -34,7 +34,7 @@ const getVersion_1 = require("./getVersion"); const fetch_1 = require("./fetch"); const root = path.dirname(path.dirname(__dirname)); const commit = (0, getVersion_1.getVersion)(root); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input) { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); return input @@ -482,9 +482,6 @@ async function esbuildExtensions(taskName, isWatch, scripts) { return reject(error); } reporter(stderr, script); - if (stderr) { - return reject(); - } return resolve(); }); proc.stdout.on('data', (data) => { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 6edfdcb63fb..5c7fa45323c 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -28,7 +28,7 @@ import { fetchUrls, fetchGithub } from './fetch'; const root = path.dirname(path.dirname(__dirname)); const commit = getVersion(root); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -563,9 +563,6 @@ async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { return reject(error); } reporter(stderr, script); - if (stderr) { - return reject(); - } return resolve(); }); diff --git a/build/lib/fetch.js b/build/lib/fetch.js index 2fed63bca0e..b7da65f4af2 100644 --- a/build/lib/fetch.js +++ b/build/lib/fetch.js @@ -33,7 +33,7 @@ function fetchUrls(urls, options) { })); } async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { - const verbose = !!options.verbose ?? (!!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']); + const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']; try { let startTime = 0; if (verbose) { diff --git a/build/lib/fetch.ts b/build/lib/fetch.ts index dc1de777e04..0c44b8e567f 100644 --- a/build/lib/fetch.ts +++ b/build/lib/fetch.ts @@ -42,7 +42,7 @@ export function fetchUrls(urls: string[] | string, options: IFetchOptions): es.T } export async function fetchUrl(url: string, options: IFetchOptions, retries = 10, retryDelay = 1000): Promise { - const verbose = !!options.verbose ?? (!!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']); + const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY']; try { let startTime = 0; if (verbose) { diff --git a/build/lib/formatter.js b/build/lib/formatter.js new file mode 100644 index 00000000000..29f265c8289 --- /dev/null +++ b/build/lib/formatter.js @@ -0,0 +1,76 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.format = format; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const fs = require("fs"); +const path = require("path"); +const ts = require("typescript"); +class LanguageServiceHost { + files = {}; + addFile(fileName, text) { + this.files[fileName] = ts.ScriptSnapshot.fromString(text); + } + fileExists(path) { + return !!this.files[path]; + } + readFile(path) { + return this.files[path]?.getText(0, this.files[path].getLength()); + } + // for ts.LanguageServiceHost + getCompilationSettings = () => ts.getDefaultCompilerOptions(); + getScriptFileNames = () => Object.keys(this.files); + getScriptVersion = (_fileName) => '0'; + getScriptSnapshot = (fileName) => this.files[fileName]; + getCurrentDirectory = () => process.cwd(); + getDefaultLibFileName = (options) => ts.getDefaultLibFilePath(options); +} +const defaults = { + baseIndentSize: 0, + indentSize: 4, + tabSize: 4, + indentStyle: ts.IndentStyle.Smart, + newLineCharacter: '\r\n', + convertTabsToSpaces: false, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterConstructor: false, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, + insertSpaceAfterTypeAssertion: false, + insertSpaceBeforeFunctionParenthesis: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, + insertSpaceBeforeTypeAnnotation: false, +}; +const getOverrides = (() => { + let value; + return () => { + value ??= JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); + return value; + }; +})(); +function format(fileName, text) { + const host = new LanguageServiceHost(); + host.addFile(fileName, text); + const languageService = ts.createLanguageService(host); + const edits = languageService.getFormattingEditsForDocument(fileName, { ...defaults, ...getOverrides() }); + edits + .sort((a, b) => a.span.start - b.span.start) + .reverse() + .forEach(edit => { + const head = text.slice(0, edit.span.start); + const tail = text.slice(edit.span.start + edit.span.length); + text = `${head}${edit.newText}${tail}`; + }); + return text; +} +//# sourceMappingURL=formatter.js.map \ No newline at end of file diff --git a/build/lib/formatter.ts b/build/lib/formatter.ts new file mode 100644 index 00000000000..0d9035b3d87 --- /dev/null +++ b/build/lib/formatter.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 * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + + +class LanguageServiceHost implements ts.LanguageServiceHost { + files: ts.MapLike = {}; + addFile(fileName: string, text: string) { + this.files[fileName] = ts.ScriptSnapshot.fromString(text); + } + + fileExists(path: string): boolean { + return !!this.files[path]; + } + + readFile(path: string): string | undefined { + return this.files[path]?.getText(0, this.files[path]!.getLength()); + } + + // for ts.LanguageServiceHost + + getCompilationSettings = () => ts.getDefaultCompilerOptions(); + getScriptFileNames = () => Object.keys(this.files); + getScriptVersion = (_fileName: string) => '0'; + getScriptSnapshot = (fileName: string) => this.files[fileName]; + getCurrentDirectory = () => process.cwd(); + getDefaultLibFileName = (options: ts.CompilerOptions) => ts.getDefaultLibFilePath(options); +} + +const defaults: ts.FormatCodeSettings = { + baseIndentSize: 0, + indentSize: 4, + tabSize: 4, + indentStyle: ts.IndentStyle.Smart, + newLineCharacter: '\r\n', + convertTabsToSpaces: false, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterConstructor: false, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, + insertSpaceAfterTypeAssertion: false, + insertSpaceBeforeFunctionParenthesis: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, + insertSpaceBeforeTypeAnnotation: false, +}; + +const getOverrides = (() => { + let value: ts.FormatCodeSettings | undefined; + return () => { + value ??= JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); + return value; + }; +})(); + +export function format(fileName: string, text: string) { + + const host = new LanguageServiceHost(); + host.addFile(fileName, text); + + const languageService = ts.createLanguageService(host); + const edits = languageService.getFormattingEditsForDocument(fileName, { ...defaults, ...getOverrides() }); + edits + .sort((a, b) => a.span.start - b.span.start) + .reverse() + .forEach(edit => { + const head = text.slice(0, edit.span.start); + const tail = text.slice(edit.span.start + edit.span.length); + text = `${head}${edit.newText}${tail}`; + }); + + return text; +} diff --git a/build/lib/i18n.js b/build/lib/i18n.js index c33994987f0..a837cbc4ac0 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -23,6 +23,7 @@ const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); const iconv = require("@vscode/iconv-lite-umd"); const l10n_dev_1 = require("@vscode/l10n-dev"); +const REPO_ROOT_PATH = path.join(__dirname, '../..'); function log(message, ...rest) { fancyLog(ansiColors.green('[i18n]'), message, ...rest); } @@ -63,6 +64,17 @@ var BundledFormat; } BundledFormat.is = is; })(BundledFormat || (BundledFormat = {})); +var NLSKeysFormat; +(function (NLSKeysFormat) { + function is(value) { + if (value === undefined) { + return false; + } + const candidate = value; + return Array.isArray(candidate) && Array.isArray(candidate[1]); + } + NLSKeysFormat.is = is; +})(NLSKeysFormat || (NLSKeysFormat = {})); class Line { buffer = []; constructor(indent = 0) { @@ -265,67 +277,8 @@ function stripComments(content) { }); return result; } -function escapeCharacters(value) { - const result = []; - for (let i = 0; i < value.length; i++) { - const ch = value.charAt(i); - switch (ch) { - case '\'': - result.push('\\\''); - break; - case '"': - result.push('\\"'); - break; - case '\\': - result.push('\\\\'); - break; - case '\n': - result.push('\\n'); - break; - case '\r': - result.push('\\r'); - break; - case '\t': - result.push('\\t'); - break; - case '\b': - result.push('\\b'); - break; - case '\f': - result.push('\\f'); - break; - default: - result.push(ch); - } - } - return result.join(''); -} -function processCoreBundleFormat(fileHeader, languages, json, emitter) { - const keysSection = json.keys; - const messageSection = json.messages; - const bundleSection = json.bundles; - const statistics = Object.create(null); - const defaultMessages = Object.create(null); - const modules = Object.keys(keysSection); - modules.forEach((module) => { - const keys = keysSection[module]; - const messages = messageSection[module]; - if (!messages || keys.length !== messages.length) { - emitter.emit('error', `Message for module ${module} corrupted. Mismatch in number of keys and messages.`); - return; - } - const messageMap = Object.create(null); - defaultMessages[module] = messageMap; - keys.map((key, i) => { - if (typeof key === 'string') { - messageMap[key] = messages[i]; - } - else { - messageMap[key.key] = messages[i]; - } - }); - }); - const languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); +function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { + const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); if (!fs.existsSync(languageDirectory)) { log(`No VS Code localization repository found. Looking at ${languageDirectory}`); log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); @@ -335,8 +288,6 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { if (process.env['VSCODE_BUILD_VERBOSE']) { log(`Generating nls bundles for: ${language.id}`); } - statistics[language.id] = 0; - const localizedModules = Object.create(null); const languageFolderName = language.translationId || language.id; const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); let allMessages; @@ -344,87 +295,36 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { const content = stripComments(fs.readFileSync(i18nFile, 'utf8')); allMessages = JSON.parse(content); } - modules.forEach((module) => { - const order = keysSection[module]; - let moduleMessage; - if (allMessages) { - moduleMessage = allMessages.contents[module]; + let nlsIndex = 0; + const nlsResult = []; + for (const [moduleId, nlsKeys] of json) { + const moduleTranslations = allMessages?.contents[moduleId]; + for (const nlsKey of nlsKeys) { + nlsResult.push(moduleTranslations?.[nlsKey]); // pushing `undefined` is fine, as we keep english strings as fallback for monaco editor in the build + nlsIndex++; } - if (!moduleMessage) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized messages found for module ${module}. Using default messages.`); - } - moduleMessage = defaultMessages[module]; - statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; - } - const localizedMessages = []; - order.forEach((keyInfo) => { - let key = null; - if (typeof keyInfo === 'string') { - key = keyInfo; - } - else { - key = keyInfo.key; - } - let message = moduleMessage[key]; - if (!message) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized message found for key ${key} in module ${module}. Using default message.`); - } - message = defaultMessages[module][key]; - statistics[language.id] = statistics[language.id] + 1; - } - localizedMessages.push(message); - }); - localizedModules[module] = localizedMessages; - }); - Object.keys(bundleSection).forEach((bundle) => { - const modules = bundleSection[bundle]; - const contents = [ - fileHeader, - `define("${bundle}.nls.${language.id}", {` - ]; - modules.forEach((module, index) => { - contents.push(`\t"${module}": [`); - const messages = localizedModules[module]; - if (!messages) { - emitter.emit('error', `Didn't find messages for module ${module}.`); - return; - } - messages.forEach((message, index) => { - contents.push(`\t\t"${escapeCharacters(message)}${index < messages.length ? '",' : '"'}`); - }); - contents.push(index < modules.length - 1 ? '\t],' : '\t]'); - }); - contents.push('});'); - emitter.queue(new File({ path: bundle + '.nls.' + language.id + '.js', contents: Buffer.from(contents.join('\n'), 'utf-8') })); - }); - }); - Object.keys(statistics).forEach(key => { - const value = statistics[key]; - log(`${key} has ${value} untranslated strings.`); - }); - sortedLanguages.forEach(language => { - const stats = statistics[language.id]; - if (!stats) { - log(`\tNo translations found for language ${language.id}. Using default language instead.`); } + emitter.queue(new File({ + contents: Buffer.from(`${fileHeader} +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)}; +globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), + base, + path: `${base}/nls.messages.${language.id}.js` + })); }); } function processNlsFiles(opts) { return (0, event_stream_1.through)(function (file) { const fileName = path.basename(file.path); - if (fileName === 'nls.metadata.json') { - let json = null; - if (file.isBuffer()) { - json = JSON.parse(file.contents.toString('utf8')); + if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles + try { + const json = JSON.parse(fs.readFileSync(path.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + if (NLSKeysFormat.is(json)) { + processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); + } } - else { - this.emit('error', `Failed to read component file: ${file.relative}`); - return; - } - if (BundledFormat.is(json)) { - processCoreBundleFormat(opts.fileHeader, opts.languages, json, this); + catch (error) { + this.emit('error', `Failed to read component file: ${error}`); } } this.queue(file); diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index b080b05f102..25c3b74b133 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -561,6 +561,10 @@ { "name": "vs/workbench/contrib/authentication", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/replNotebook", + "project": "vscode-workbench" } ] } diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 444e3abe59c..28f8cc993e6 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -16,6 +16,8 @@ import * as ansiColors from 'ansi-colors'; import * as iconv from '@vscode/iconv-lite-umd'; import { l10nJsonFormat, getL10nXlf, l10nJsonDetails, getL10nFilesFromXlf, getL10nJson } from '@vscode/l10n-dev'; +const REPO_ROOT_PATH = path.join(__dirname, '../..'); + function log(message: any, ...rest: any[]): void { fancyLog(ansiColors.green('[i18n]'), message, ...rest); } @@ -91,6 +93,19 @@ module BundledFormat { } } +type NLSKeysFormat = [string /* module ID */, string[] /* keys */]; + +module NLSKeysFormat { + export function is(value: any): value is NLSKeysFormat { + if (value === undefined) { + return false; + } + + const candidate = value as NLSKeysFormat; + return Array.isArray(candidate) && Array.isArray(candidate[1]); + } +} + interface BundledExtensionFormat { [key: string]: { messages: string[]; @@ -329,70 +344,8 @@ function stripComments(content: string): string { return result; } -function escapeCharacters(value: string): string { - const result: string[] = []; - for (let i = 0; i < value.length; i++) { - const ch = value.charAt(i); - switch (ch) { - case '\'': - result.push('\\\''); - break; - case '"': - result.push('\\"'); - break; - case '\\': - result.push('\\\\'); - break; - case '\n': - result.push('\\n'); - break; - case '\r': - result.push('\\r'); - break; - case '\t': - result.push('\\t'); - break; - case '\b': - result.push('\\b'); - break; - case '\f': - result.push('\\f'); - break; - default: - result.push(ch); - } - } - return result.join(''); -} - -function processCoreBundleFormat(fileHeader: string, languages: Language[], json: BundledFormat, emitter: ThroughStream) { - const keysSection = json.keys; - const messageSection = json.messages; - const bundleSection = json.bundles; - - const statistics: Record = Object.create(null); - - const defaultMessages: Record> = Object.create(null); - const modules = Object.keys(keysSection); - modules.forEach((module) => { - const keys = keysSection[module]; - const messages = messageSection[module]; - if (!messages || keys.length !== messages.length) { - emitter.emit('error', `Message for module ${module} corrupted. Mismatch in number of keys and messages.`); - return; - } - const messageMap: Record = Object.create(null); - defaultMessages[module] = messageMap; - keys.map((key, i) => { - if (typeof key === 'string') { - messageMap[key] = messages[i]; - } else { - messageMap[key.key] = messages[i]; - } - }); - }); - - const languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); +function processCoreBundleFormat(base: string, fileHeader: string, languages: Language[], json: NLSKeysFormat, emitter: ThroughStream) { + const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); if (!fs.existsSync(languageDirectory)) { log(`No VS Code localization repository found. Looking at ${languageDirectory}`); log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); @@ -403,8 +356,6 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json log(`Generating nls bundles for: ${language.id}`); } - statistics[language.id] = 0; - const localizedModules: Record = Object.create(null); const languageFolderName = language.translationId || language.id; const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); let allMessages: I18nFormat | undefined; @@ -412,86 +363,38 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json const content = stripComments(fs.readFileSync(i18nFile, 'utf8')); allMessages = JSON.parse(content); } - modules.forEach((module) => { - const order = keysSection[module]; - let moduleMessage: { [messageKey: string]: string } | undefined; - if (allMessages) { - moduleMessage = allMessages.contents[module]; + + let nlsIndex = 0; + const nlsResult: Array = []; + for (const [moduleId, nlsKeys] of json) { + const moduleTranslations = allMessages?.contents[moduleId]; + for (const nlsKey of nlsKeys) { + nlsResult.push(moduleTranslations?.[nlsKey]); // pushing `undefined` is fine, as we keep english strings as fallback for monaco editor in the build + nlsIndex++; } - if (!moduleMessage) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized messages found for module ${module}. Using default messages.`); - } - moduleMessage = defaultMessages[module]; - statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; - } - const localizedMessages: string[] = []; - order.forEach((keyInfo) => { - let key: string | null = null; - if (typeof keyInfo === 'string') { - key = keyInfo; - } else { - key = keyInfo.key; - } - let message: string = moduleMessage![key]; - if (!message) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized message found for key ${key} in module ${module}. Using default message.`); - } - message = defaultMessages[module][key]; - statistics[language.id] = statistics[language.id] + 1; - } - localizedMessages.push(message); - }); - localizedModules[module] = localizedMessages; - }); - Object.keys(bundleSection).forEach((bundle) => { - const modules = bundleSection[bundle]; - const contents: string[] = [ - fileHeader, - `define("${bundle}.nls.${language.id}", {` - ]; - modules.forEach((module, index) => { - contents.push(`\t"${module}": [`); - const messages = localizedModules[module]; - if (!messages) { - emitter.emit('error', `Didn't find messages for module ${module}.`); - return; - } - messages.forEach((message, index) => { - contents.push(`\t\t"${escapeCharacters(message)}${index < messages.length ? '",' : '"'}`); - }); - contents.push(index < modules.length - 1 ? '\t],' : '\t]'); - }); - contents.push('});'); - emitter.queue(new File({ path: bundle + '.nls.' + language.id + '.js', contents: Buffer.from(contents.join('\n'), 'utf-8') })); - }); - }); - Object.keys(statistics).forEach(key => { - const value = statistics[key]; - log(`${key} has ${value} untranslated strings.`); - }); - sortedLanguages.forEach(language => { - const stats = statistics[language.id]; - if (!stats) { - log(`\tNo translations found for language ${language.id}. Using default language instead.`); } + + emitter.queue(new File({ + contents: Buffer.from(`${fileHeader} +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)}; +globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), + base, + path: `${base}/nls.messages.${language.id}.js` + })); }); } -export function processNlsFiles(opts: { fileHeader: string; languages: Language[] }): ThroughStream { +export function processNlsFiles(opts: { out: string; fileHeader: string; languages: Language[] }): ThroughStream { return through(function (this: ThroughStream, file: File) { const fileName = path.basename(file.path); - if (fileName === 'nls.metadata.json') { - let json = null; - if (file.isBuffer()) { - json = JSON.parse((file.contents).toString('utf8')); - } else { - this.emit('error', `Failed to read component file: ${file.relative}`); - return; - } - if (BundledFormat.is(json)) { - processCoreBundleFormat(opts.fileHeader, opts.languages, json, this); + if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles + try { + const json = JSON.parse(fs.readFileSync(path.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + if (NLSKeysFormat.is(json)) { + processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); + } + } catch (error) { + this.emit('error', `Failed to read component file: ${error}`); } } this.queue(file); diff --git a/build/lib/inlineMeta.js b/build/lib/inlineMeta.js new file mode 100644 index 00000000000..f1dbfa83a7e --- /dev/null +++ b/build/lib/inlineMeta.js @@ -0,0 +1,48 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.inlineMeta = inlineMeta; +const es = require("event-stream"); +const path_1 = require("path"); +const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; +// TODO@bpasero in order to inline `product.json`, more work is +// needed to ensure that we cover all cases where modifications +// are done to the product configuration during build. There are +// at least 2 more changes that kick in very late: +// - a `darwinUniversalAssetId` is added in`create-universal-app.ts` +// - a `target` is added in `gulpfile.vscode.win32.js` +// const productJsonMarkerId = 'BUILD_INSERT_PRODUCT_CONFIGURATION'; +function inlineMeta(result, ctx) { + return result.pipe(es.through(function (file) { + if (matchesFile(file, ctx)) { + let content = file.contents.toString(); + let markerFound = false; + const packageMarker = `${packageJsonMarkerId}:"${packageJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + if (content.includes(packageMarker)) { + content = content.replace(packageMarker, JSON.stringify(JSON.parse(ctx.packageJsonFn())).slice(1, -1) /* trim braces */); + markerFound = true; + } + // const productMarker = `${productJsonMarkerId}:"${productJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + // if (content.includes(productMarker)) { + // content = content.replace(productMarker, JSON.stringify(JSON.parse(ctx.productJsonFn())).slice(1, -1) /* trim braces */); + // markerFound = true; + // } + if (markerFound) { + file.contents = Buffer.from(content); + } + } + this.emit('data', file); + })); +} +function matchesFile(file, ctx) { + for (const targetPath of ctx.targetPaths) { + if (file.basename === (0, path_1.basename)(targetPath)) { // TODO would be nicer to figure out root relative path to not match on false positives + return true; + } + } + return false; +} +//# sourceMappingURL=inlineMeta.js.map \ No newline at end of file diff --git a/build/lib/inlineMeta.ts b/build/lib/inlineMeta.ts new file mode 100644 index 00000000000..ef3987fc32e --- /dev/null +++ b/build/lib/inlineMeta.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as es from 'event-stream'; +import { basename } from 'path'; +import * as File from 'vinyl'; + +export interface IInlineMetaContext { + readonly targetPaths: string[]; + readonly packageJsonFn: () => string; + readonly productJsonFn: () => string; +} + +const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; + +// TODO@bpasero in order to inline `product.json`, more work is +// needed to ensure that we cover all cases where modifications +// are done to the product configuration during build. There are +// at least 2 more changes that kick in very late: +// - a `darwinUniversalAssetId` is added in`create-universal-app.ts` +// - a `target` is added in `gulpfile.vscode.win32.js` +// const productJsonMarkerId = 'BUILD_INSERT_PRODUCT_CONFIGURATION'; + +export function inlineMeta(result: NodeJS.ReadWriteStream, ctx: IInlineMetaContext): NodeJS.ReadWriteStream { + return result.pipe(es.through(function (file: File) { + if (matchesFile(file, ctx)) { + let content = file.contents.toString(); + let markerFound = false; + + const packageMarker = `${packageJsonMarkerId}:"${packageJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + if (content.includes(packageMarker)) { + content = content.replace(packageMarker, JSON.stringify(JSON.parse(ctx.packageJsonFn())).slice(1, -1) /* trim braces */); + markerFound = true; + } + + // const productMarker = `${productJsonMarkerId}:"${productJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + // if (content.includes(productMarker)) { + // content = content.replace(productMarker, JSON.stringify(JSON.parse(ctx.productJsonFn())).slice(1, -1) /* trim braces */); + // markerFound = true; + // } + + if (markerFound) { + file.contents = Buffer.from(content); + } + } + + this.emit('data', file); + })); +} + +function matchesFile(file: File, ctx: IInlineMetaContext): boolean { + for (const targetPath of ctx.targetPaths) { + if (file.basename === basename(targetPath)) { // TODO would be nicer to figure out root relative path to not match on false positives + return true; + } + } + return false; +} diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 4979682935e..7c7fd3bc5fd 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -62,7 +62,16 @@ const CORE_TYPES = [ 'EventTarget', 'BroadcastChannel', 'performance', - 'Blob' + 'Blob', + 'crypto', + 'File', + 'fetch', + 'RequestInit', + 'Headers', + 'Response', + '__global', + 'PerformanceMark', + 'PerformanceObserver', ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser @@ -164,6 +173,20 @@ const RULES = [ '@types/node' // no node.js ] }, + // Common: vs/base/parts/sandbox/electron-sandbox/preload.js + { + target: '**/vs/base/parts/sandbox/electron-sandbox/preload.js', + allowedTypes: [ + ...CORE_TYPES, + // Safe access to a very small subset of node.js + 'process', + 'NodeJS' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, // Common { target: '**/vs/**/common/**', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 864d61f1452..60939fe2750 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -63,7 +63,16 @@ const CORE_TYPES = [ 'EventTarget', 'BroadcastChannel', 'performance', - 'Blob' + 'Blob', + 'crypto', + 'File', + 'fetch', + 'RequestInit', + 'Headers', + 'Response', + '__global', + 'PerformanceMark', + 'PerformanceObserver', ]; // Types that are defined in a common layer but are known to be only @@ -179,6 +188,22 @@ const RULES: IRule[] = [ ] }, + // Common: vs/base/parts/sandbox/electron-sandbox/preload.js + { + target: '**/vs/base/parts/sandbox/electron-sandbox/preload.js', + allowedTypes: [ + ...CORE_TYPES, + + // Safe access to a very small subset of node.js + 'process', + 'NodeJS' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, + // Common { target: '**/vs/**/common/**', diff --git a/build/lib/mangle/index.js b/build/lib/mangle/index.js index bb6b414e845..10e1aeb0d5a 100644 --- a/build/lib/mangle/index.js +++ b/build/lib/mangle/index.js @@ -250,7 +250,6 @@ function isNameTakenInFile(node, name) { const skippedExportMangledFiles = [ // Build 'css.build', - 'nls.build', // Monaco 'editorCommon', 'editorOptions', diff --git a/build/lib/mangle/index.ts b/build/lib/mangle/index.ts index 4a7544f162b..a148c4dd637 100644 --- a/build/lib/mangle/index.ts +++ b/build/lib/mangle/index.ts @@ -283,7 +283,6 @@ function isNameTakenInFile(node: ts.Node, name: string): boolean { const skippedExportMangledFiles = [ // Build 'css.build', - 'nls.build', // Monaco 'editorCommon', diff --git a/build/lib/nls.js b/build/lib/nls.js index 48ca84f2433..107a8d4162e 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -10,6 +10,7 @@ const event_stream_1 = require("event-stream"); const File = require("vinyl"); const sm = require("source-map"); const path = require("path"); +const sort = require("gulp-sort"); var CollectStepResult; (function (CollectStepResult) { CollectStepResult[CollectStepResult["Yes"] = 0] = "Yes"; @@ -38,23 +39,15 @@ function clone(object) { } return result; } -function template(lines) { - let indent = '', wrap = ''; - if (lines.length > 1) { - indent = '\t'; - wrap = '\n'; - } - return `/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -define([], [${wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; -} /** * Returns a stream containing the patched JavaScript and source maps. */ -function nls() { +function nls(options) { + let base; const input = (0, event_stream_1.through)(); - const output = input.pipe((0, event_stream_1.through)(function (f) { + const output = input + .pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files + .pipe((0, event_stream_1.through)(function (f) { if (!f.sourceMap) { return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); } @@ -70,7 +63,40 @@ function nls() { if (!typescript) { return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); } - _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); + base = f.base; + this.emit('data', _nls.patchFile(f, typescript, options)); + }, function () { + for (const file of [ + new File({ + contents: Buffer.from(JSON.stringify({ + keys: _nls.moduleToNLSKeys, + messages: _nls.moduleToNLSMessages, + }, null, '\t')), + base, + path: `${base}/nls.metadata.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), + base, + path: `${base}/nls.messages.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), + base, + path: `${base}/nls.keys.json` + }), + new File({ + contents: Buffer.from(`/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), + base, + path: `${base}/nls.messages.js` + }) + ]) { + this.emit('data', file); + } + this.emit('end'); })); return (0, event_stream_1.duplex)(input, output); } @@ -79,6 +105,11 @@ function isImportNode(ts, node) { } var _nls; (function (_nls) { + _nls.moduleToNLSKeys = {}; + _nls.moduleToNLSMessages = {}; + _nls.allNLSMessages = []; + _nls.allNLSModulesAndKeys = []; + let allNLSMessagesIndex = 0; function fileFrom(file, contents, path = file.path) { return new File({ contents: Buffer.from(contents), @@ -146,13 +177,6 @@ var _nls; .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) .filter(d => d.moduleSpecifier.getText() === '\'vs/nls\'') .filter(d => !!d.importClause && !!d.importClause.namedBindings); - const nlsExpressions = importEqualsDeclarations - .map(d => d.moduleReference.expression) - .concat(importDeclarations.map(d => d.moduleSpecifier)) - .map(d => ({ - start: ts.getLineAndCharacterOfPosition(sourceFile, d.getStart()), - end: ts.getLineAndCharacterOfPosition(sourceFile, d.getEnd()) - })); // `nls.localize(...)` calls const nlsLocalizeCallExpressions = importDeclarations .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) @@ -206,8 +230,7 @@ var _nls; value: a[1].getText() })); return { - localizeCalls: localizeCalls.toArray(), - nlsExpressions: nlsExpressions.toArray() + localizeCalls: localizeCalls.toArray() }; } class TextModel { @@ -262,14 +285,10 @@ var _nls; .flatten().toArray().join(''); } } - function patchJavascript(patches, contents, moduleId) { + function patchJavascript(patches, contents) { const model = new TextModel(contents); // patch the localize calls lazy(patches).reverse().each(p => model.apply(p)); - // patch the 'vs/nls' imports - const firstLine = model.get(0); - const patchedFirstLine = firstLine.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - model.set(0, patchedFirstLine); return model.toString(); } function patchSourcemap(patches, rsm, smc) { @@ -307,14 +326,21 @@ var _nls; } return JSON.parse(smg.toString()); } - function patch(ts, moduleId, typescript, javascript, sourcemap) { - const { localizeCalls, nlsExpressions } = analyze(ts, typescript, 'localize'); - const { localizeCalls: localize2Calls, nlsExpressions: nls2Expressions } = analyze(ts, typescript, 'localize2'); + function parseLocalizeKeyOrValue(sourceExpression) { + // sourceValue can be "foo", 'foo', `foo` or { .... } + // in its evalulated form + // we want to return either the string or the object + // eslint-disable-next-line no-eval + return eval(`(${sourceExpression})`); + } + function patch(ts, typescript, javascript, sourcemap, options) { + const { localizeCalls } = analyze(ts, typescript, 'localize'); + const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2'); if (localizeCalls.length === 0 && localize2Calls.length === 0) { return { javascript, sourcemap }; } - const nlsKeys = template(localizeCalls.map(lc => lc.key).concat(localize2Calls.map(lc => lc.key))); - const nls = template(localizeCalls.map(lc => lc.value).concat(localize2Calls.map(lc => lc.value))); + const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); + const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); const smc = new sm.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); // build patches @@ -323,16 +349,18 @@ var _nls; const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end))); return { span: { start, end }, content: c.content }; }; - let i = 0; const localizePatches = lazy(localizeCalls) - .map(lc => ([ - { range: lc.keySpan, content: '' + (i++) }, + .map(lc => (options.preserveEnglish ? [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(, "message") + ] : [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` }, // localize('key', "message") => localize(, null) { range: lc.valueSpan, content: 'null' } ])) .flatten() .map(toPatch); const localize2Patches = lazy(localize2Calls) - .map(lc => ({ range: lc.keySpan, content: '' + (i++) })) + .map(lc => ({ range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(, "message") + )) .map(toPatch); // Sort patches by their start position const patches = localizePatches.concat(localize2Patches).toArray().sort((a, b) => { @@ -352,34 +380,29 @@ var _nls; return 0; } }); - javascript = patchJavascript(patches, javascript, moduleId); - // since imports are not within the sourcemap information, - // we must do this MacGyver style - if (nlsExpressions.length || nls2Expressions.length) { - javascript = javascript.replace(/^define\(.*$/m, line => { - return line.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - }); - } + javascript = patchJavascript(patches, javascript); sourcemap = patchSourcemap(patches, sourcemap, smc); - return { javascript, sourcemap, nlsKeys, nls }; + return { javascript, sourcemap, nlsKeys, nlsMessages }; } - function patchFiles(javascriptFile, typescript) { + function patchFile(javascriptFile, typescript, options) { const ts = require('typescript'); // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') .replace(/\\/g, '/'); - const { javascript, sourcemap, nlsKeys, nls } = patch(ts, moduleId, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap); - const result = [fileFrom(javascriptFile, javascript)]; - result[0].sourceMap = sourcemap; + const { javascript, sourcemap, nlsKeys, nlsMessages } = patch(ts, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap, options); + const result = fileFrom(javascriptFile, javascript); + result.sourceMap = sourcemap; if (nlsKeys) { - result.push(fileFrom(javascriptFile, nlsKeys, javascriptFile.path.replace(/\.js$/, '.nls.keys.js'))); + _nls.moduleToNLSKeys[moduleId] = nlsKeys; + _nls.allNLSModulesAndKeys.push([moduleId, nlsKeys.map(nlsKey => typeof nlsKey === 'string' ? nlsKey : nlsKey.key)]); } - if (nls) { - result.push(fileFrom(javascriptFile, nls, javascriptFile.path.replace(/\.js$/, '.nls.js'))); + if (nlsMessages) { + _nls.moduleToNLSMessages[moduleId] = nlsMessages; + _nls.allNLSMessages.push(...nlsMessages); } return result; } - _nls.patchFiles = patchFiles; + _nls.patchFile = patchFile; })(_nls || (_nls = {})); //# sourceMappingURL=nls.js.map \ No newline at end of file diff --git a/build/lib/nls.ts b/build/lib/nls.ts index c4ee031b2eb..1861eca6287 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -8,7 +8,8 @@ import * as lazy from 'lazy.js'; import { duplex, through } from 'event-stream'; import * as File from 'vinyl'; import * as sm from 'source-map'; -import * as path from 'path'; +import * as path from 'path'; +import * as sort from 'gulp-sort'; declare class FileSourceMap extends File { public sourceMap: sm.RawSourceMap; @@ -48,47 +49,70 @@ function clone(object: T): T { return result; } -function template(lines: string[]): string { - let indent = '', wrap = ''; - - if (lines.length > 1) { - indent = '\t'; - wrap = '\n'; - } - - return `/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -define([], [${wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; -} - /** * Returns a stream containing the patched JavaScript and source maps. */ -export function nls(): NodeJS.ReadWriteStream { +export function nls(options: { preserveEnglish: boolean }): NodeJS.ReadWriteStream { + let base: string; const input = through(); - const output = input.pipe(through(function (f: FileSourceMap) { - if (!f.sourceMap) { - return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); - } + const output = input + .pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files + .pipe(through(function (f: FileSourceMap) { + if (!f.sourceMap) { + return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); + } - let source = f.sourceMap.sources[0]; - if (!source) { - return this.emit('error', new Error(`File ${f.relative} does not have a source in the source map.`)); - } + let source = f.sourceMap.sources[0]; + if (!source) { + return this.emit('error', new Error(`File ${f.relative} does not have a source in the source map.`)); + } - const root = f.sourceMap.sourceRoot; - if (root) { - source = path.join(root, source); - } + const root = f.sourceMap.sourceRoot; + if (root) { + source = path.join(root, source); + } - const typescript = f.sourceMap.sourcesContent![0]; - if (!typescript) { - return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); - } + const typescript = f.sourceMap.sourcesContent![0]; + if (!typescript) { + return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); + } - _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); - })); + base = f.base; + this.emit('data', _nls.patchFile(f, typescript, options)); + }, function () { + for (const file of [ + new File({ + contents: Buffer.from(JSON.stringify({ + keys: _nls.moduleToNLSKeys, + messages: _nls.moduleToNLSMessages, + }, null, '\t')), + base, + path: `${base}/nls.metadata.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), + base, + path: `${base}/nls.messages.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), + base, + path: `${base}/nls.keys.json` + }), + new File({ + contents: Buffer.from(`/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), + base, + path: `${base}/nls.messages.js` + }) + ]) { + this.emit('data', file); + } + + this.emit('end'); + })); return duplex(input, output); } @@ -99,11 +123,19 @@ function isImportNode(ts: typeof import('typescript'), node: ts.Node): boolean { module _nls { - interface INlsStringResult { + export const moduleToNLSKeys: { [name: string /* module ID */]: ILocalizeKey[] /* keys */ } = {}; + export const moduleToNLSMessages: { [name: string /* module ID */]: string[] /* messages */ } = {}; + export const allNLSMessages: string[] = []; + export const allNLSModulesAndKeys: Array<[string /* module ID */, string[] /* keys */]> = []; + let allNLSMessagesIndex = 0; + + type ILocalizeKey = string | { key: string }; // key might contain metadata for translators and then is not just a string + + interface INlsPatchResult { javascript: string; sourcemap: sm.RawSourceMap; - nls?: string; - nlsKeys?: string; + nlsMessages?: string[]; + nlsKeys?: ILocalizeKey[]; } interface ISpan { @@ -120,7 +152,6 @@ module _nls { interface ILocalizeAnalysisResult { localizeCalls: ILocalizeCall[]; - nlsExpressions: ISpan[]; } interface IPatch { @@ -210,14 +241,6 @@ module _nls { .filter(d => d.moduleSpecifier.getText() === '\'vs/nls\'') .filter(d => !!d.importClause && !!d.importClause.namedBindings); - const nlsExpressions = importEqualsDeclarations - .map(d => (d.moduleReference).expression) - .concat(importDeclarations.map(d => d.moduleSpecifier)) - .map(d => ({ - start: ts.getLineAndCharacterOfPosition(sourceFile, d.getStart()), - end: ts.getLineAndCharacterOfPosition(sourceFile, d.getEnd()) - })); - // `nls.localize(...)` calls const nlsLocalizeCallExpressions = importDeclarations .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) @@ -280,8 +303,7 @@ module _nls { })); return { - localizeCalls: localizeCalls.toArray(), - nlsExpressions: nlsExpressions.toArray() + localizeCalls: localizeCalls.toArray() }; } @@ -351,17 +373,12 @@ module _nls { } } - function patchJavascript(patches: IPatch[], contents: string, moduleId: string): string { + function patchJavascript(patches: IPatch[], contents: string): string { const model = new TextModel(contents); // patch the localize calls lazy(patches).reverse().each(p => model.apply(p)); - // patch the 'vs/nls' imports - const firstLine = model.get(0); - const patchedFirstLine = firstLine.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - model.set(0, patchedFirstLine); - return model.toString(); } @@ -410,16 +427,24 @@ module _nls { return JSON.parse(smg.toString()); } - function patch(ts: typeof import('typescript'), moduleId: string, typescript: string, javascript: string, sourcemap: sm.RawSourceMap): INlsStringResult { - const { localizeCalls, nlsExpressions } = analyze(ts, typescript, 'localize'); - const { localizeCalls: localize2Calls, nlsExpressions: nls2Expressions } = analyze(ts, typescript, 'localize2'); + function parseLocalizeKeyOrValue(sourceExpression: string) { + // sourceValue can be "foo", 'foo', `foo` or { .... } + // in its evalulated form + // we want to return either the string or the object + // eslint-disable-next-line no-eval + return eval(`(${sourceExpression})`); + } + + function patch(ts: typeof import('typescript'), typescript: string, javascript: string, sourcemap: sm.RawSourceMap, options: { preserveEnglish: boolean }): INlsPatchResult { + const { localizeCalls } = analyze(ts, typescript, 'localize'); + const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2'); if (localizeCalls.length === 0 && localize2Calls.length === 0) { return { javascript, sourcemap }; } - const nlsKeys = template(localizeCalls.map(lc => lc.key).concat(localize2Calls.map(lc => lc.key))); - const nls = template(localizeCalls.map(lc => lc.value).concat(localize2Calls.map(lc => lc.value))); + const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); + const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); const smc = new sm.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); @@ -430,18 +455,20 @@ module _nls { return { span: { start, end }, content: c.content }; }; - let i = 0; const localizePatches = lazy(localizeCalls) - .map(lc => ([ - { range: lc.keySpan, content: '' + (i++) }, - { range: lc.valueSpan, content: 'null' } - ])) + .map(lc => ( + options.preserveEnglish ? [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(, "message") + ] : [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` }, // localize('key', "message") => localize(, null) + { range: lc.valueSpan, content: 'null' } + ])) .flatten() .map(toPatch); const localize2Patches = lazy(localize2Calls) .map(lc => ( - { range: lc.keySpan, content: '' + (i++) } + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(, "message") )) .map(toPatch); @@ -460,45 +487,39 @@ module _nls { } }); - javascript = patchJavascript(patches, javascript, moduleId); - - // since imports are not within the sourcemap information, - // we must do this MacGyver style - if (nlsExpressions.length || nls2Expressions.length) { - javascript = javascript.replace(/^define\(.*$/m, line => { - return line.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - }); - } + javascript = patchJavascript(patches, javascript); sourcemap = patchSourcemap(patches, sourcemap, smc); - return { javascript, sourcemap, nlsKeys, nls }; + return { javascript, sourcemap, nlsKeys, nlsMessages }; } - export function patchFiles(javascriptFile: File, typescript: string): File[] { + export function patchFile(javascriptFile: File, typescript: string, options: { preserveEnglish: boolean }): File { const ts = require('typescript') as typeof import('typescript'); // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') .replace(/\\/g, '/'); - const { javascript, sourcemap, nlsKeys, nls } = patch( + const { javascript, sourcemap, nlsKeys, nlsMessages } = patch( ts, - moduleId, typescript, javascriptFile.contents.toString(), - (javascriptFile).sourceMap + (javascriptFile).sourceMap, + options ); - const result: File[] = [fileFrom(javascriptFile, javascript)]; - (result[0]).sourceMap = sourcemap; + const result = fileFrom(javascriptFile, javascript); + (result).sourceMap = sourcemap; if (nlsKeys) { - result.push(fileFrom(javascriptFile, nlsKeys, javascriptFile.path.replace(/\.js$/, '.nls.keys.js'))); + moduleToNLSKeys[moduleId] = nlsKeys; + allNLSModulesAndKeys.push([moduleId, nlsKeys.map(nlsKey => typeof nlsKey === 'string' ? nlsKey : nlsKey.key)]); } - if (nls) { - result.push(fileFrom(javascriptFile, nls, javascriptFile.path.replace(/\.js$/, '.nls.js'))); + if (nlsMessages) { + moduleToNLSMessages[moduleId] = nlsMessages; + allNLSMessages.push(...nlsMessages); } return result; diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 237f2bc20e8..79509e9a2d5 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -21,6 +21,7 @@ const bundle = require("./bundle"); const i18n_1 = require("./i18n"); const stats_1 = require("./stats"); const util = require("./util"); +const postcss_1 = require("./postcss"); const REPO_ROOT_PATH = path.join(__dirname, '../..'); function log(prefix, message) { fancyLog(ansiColors.cyan('[' + prefix + ']'), message); @@ -52,7 +53,7 @@ function loaderPlugin(src, base, amdModuleId) { function loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo) { let loaderStream = gulp.src(`${src}/vs/loader.js`, { base: `${src}` }); if (bundleLoader) { - loaderStream = es.merge(loaderStream, loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css'), loaderPlugin(`${src}/vs/nls.js`, `${src}`, 'vs/nls')); + loaderStream = es.merge(loaderStream, loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css')); } const files = []; const order = (f) => { @@ -62,10 +63,7 @@ function loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo) { if (f.path.endsWith('css.js')) { return 1; } - if (f.path.endsWith('nls.js')) { - return 2; - } - return 3; + return 2; }; return (loaderStream .pipe(es.through(function (data) { @@ -191,6 +189,7 @@ function optimizeAMDTask(opts) { includeContent: true })) .pipe(opts.languages && opts.languages.length ? (0, i18n_1.processNlsFiles)({ + out: opts.src, fileHeader: bundledFileHeader, languages: opts.languages }) : es.through()); @@ -242,7 +241,6 @@ function minifyTask(src, sourceMapBaseUrl) { const sourceMappingURL = sourceMapBaseUrl ? ((f) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; return cb => { const cssnano = require('cssnano'); - const postcss = require('gulp-postcss'); const sourcemaps = require('gulp-sourcemaps'); const svgmin = require('gulp-svgmin'); const jsFilter = filter('**/*.js', { restore: true }); @@ -271,7 +269,7 @@ function minifyTask(src, sourceMapBaseUrl) { cb(undefined, f); } }, cb); - }), jsFilter.restore, cssFilter, postcss([cssnano({ preset: 'default' })]), cssFilter.restore, svgFilter, svgmin(), svgFilter.restore, sourcemaps.mapSources((sourcePath) => { + }), jsFilter.restore, cssFilter, (0, postcss_1.gulpPostcss)([cssnano({ preset: 'default' })]), cssFilter.restore, svgFilter, svgmin(), svgFilter.restore, sourcemaps.mapSources((sourcePath) => { if (sourcePath === 'bootstrap-fork.js') { return 'bootstrap-fork.orig.js'; } diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index aebe22a7e0a..6f9786b4d1e 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -16,6 +16,7 @@ import * as bundle from './bundle'; import { Language, processNlsFiles } from './i18n'; import { createStatsStream } from './stats'; import * as util from './util'; +import { gulpPostcss } from './postcss'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); @@ -59,8 +60,7 @@ function loader(src: string, bundledFileHeader: string, bundleLoader: boolean, e if (bundleLoader) { loaderStream = es.merge( loaderStream, - loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css'), - loaderPlugin(`${src}/vs/nls.js`, `${src}`, 'vs/nls'), + loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css') ); } @@ -72,10 +72,7 @@ function loader(src: string, bundledFileHeader: string, bundleLoader: boolean, e if (f.path.endsWith('css.js')) { return 1; } - if (f.path.endsWith('nls.js')) { - return 2; - } - return 3; + return 2; }; return ( @@ -268,6 +265,7 @@ function optimizeAMDTask(opts: IOptimizeAMDTaskOpts): NodeJS.ReadWriteStream { includeContent: true })) .pipe(opts.languages && opts.languages.length ? processNlsFiles({ + out: opts.src, fileHeader: bundledFileHeader, languages: opts.languages }) : es.through()); @@ -381,7 +379,6 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => return cb => { const cssnano = require('cssnano') as typeof import('cssnano'); - const postcss = require('gulp-postcss') as typeof import('gulp-postcss'); const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); const svgmin = require('gulp-svgmin') as typeof import('gulp-svgmin'); @@ -420,7 +417,7 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => }), jsFilter.restore, cssFilter, - postcss([cssnano({ preset: 'default' })]), + gulpPostcss([cssnano({ preset: 'default' })]), cssFilter.restore, svgFilter, svgmin(), diff --git a/build/lib/postcss.js b/build/lib/postcss.js new file mode 100644 index 00000000000..356015ab159 --- /dev/null +++ b/build/lib/postcss.js @@ -0,0 +1,36 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.gulpPostcss = gulpPostcss; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const postcss = require("postcss"); +const es = require("event-stream"); +function gulpPostcss(plugins, handleError) { + const instance = postcss(plugins); + return es.map((file, callback) => { + if (file.isNull()) { + return callback(null, file); + } + if (file.isStream()) { + return callback(new Error('Streaming not supported')); + } + instance + .process(file.contents.toString(), { from: file.path }) + .then((result) => { + file.contents = Buffer.from(result.css); + callback(null, file); + }) + .catch((error) => { + if (handleError) { + handleError(error); + callback(); + } + else { + callback(error); + } + }); + }); +} +//# sourceMappingURL=postcss.js.map \ No newline at end of file diff --git a/build/lib/postcss.ts b/build/lib/postcss.ts new file mode 100644 index 00000000000..cf3121e221e --- /dev/null +++ b/build/lib/postcss.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 * as postcss from 'postcss'; +import * as File from 'vinyl'; +import * as es from 'event-stream'; + +export function gulpPostcss(plugins: postcss.AcceptedPlugin[], handleError?: (err: Error) => void) { + const instance = postcss(plugins); + + return es.map((file: File, callback: (error?: any, file?: any) => void) => { + if (file.isNull()) { + return callback(null, file); + } + + if (file.isStream()) { + return callback(new Error('Streaming not supported')); + } + + instance + .process(file.contents.toString(), { from: file.path }) + .then((result) => { + file.contents = Buffer.from(result.css); + callback(null, file); + }) + .catch((error) => { + if (handleError) { + handleError(error); + callback(); + } else { + callback(error); + } + }); + }); +} diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js index efcb3220084..1bfe7f573f6 100644 --- a/build/lib/preLaunch.js +++ b/build/lib/preLaunch.js @@ -12,7 +12,7 @@ const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command, args = []) { return new Promise((resolve, reject) => { - const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); }); diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index 3d3f513b591..d6776e62798 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -14,7 +14,7 @@ const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command: string, args: ReadonlyArray = []) { return new Promise((resolve, reject) => { - const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); }); diff --git a/build/lib/standalone.js b/build/lib/standalone.js index dbc47db0833..cf0e452aff3 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -106,10 +106,7 @@ function extractEditor(options) { 'vs/css.build.ts', 'vs/css.ts', 'vs/loader.js', - 'vs/loader.d.ts', - 'vs/nls.build.ts', - 'vs/nls.ts', - 'vs/nls.mock.ts', + 'vs/loader.d.ts' ].forEach(copyFile); } function createESMSourcesAndResources2(options) { diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 775a1be5996..9a65bfa7444 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -118,10 +118,7 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str 'vs/css.build.ts', 'vs/css.ts', 'vs/loader.js', - 'vs/loader.d.ts', - 'vs/nls.build.ts', - 'vs/nls.ts', - 'vs/nls.mock.ts', + 'vs/loader.d.ts' ].forEach(copyFile); } diff --git a/build/lib/stylelint/validateVariableNames.js b/build/lib/stylelint/validateVariableNames.js index 57b2aad957f..6a50d1d6894 100644 --- a/build/lib/stylelint/validateVariableNames.js +++ b/build/lib/stylelint/validateVariableNames.js @@ -17,6 +17,7 @@ function getKnownVariableNames() { } return knownVariables; } +const iconVariable = /^--vscode-icon-.+-(content|font-family)$/; function getVariableNameValidator() { const allVariables = getKnownVariableNames(); return (value, report) => { @@ -24,7 +25,7 @@ function getVariableNameValidator() { let match; while (match = RE_VAR_PROP.exec(value)) { const variableName = match[1]; - if (variableName && !allVariables.has(variableName)) { + if (variableName && !allVariables.has(variableName) && !iconVariable.test(variableName)) { report(variableName); } } diff --git a/build/lib/stylelint/validateVariableNames.ts b/build/lib/stylelint/validateVariableNames.ts index 56e5f84a81f..6d9fa8a7cef 100644 --- a/build/lib/stylelint/validateVariableNames.ts +++ b/build/lib/stylelint/validateVariableNames.ts @@ -18,6 +18,8 @@ function getKnownVariableNames() { return knownVariables; } +const iconVariable = /^--vscode-icon-.+-(content|font-family)$/; + export interface IValidator { (value: string, report: (message: string) => void): void; } @@ -29,7 +31,7 @@ export function getVariableNameValidator(): IValidator { let match; while (match = RE_VAR_PROP.exec(value)) { const variableName = match[1]; - if (variableName && !allVariables.has(variableName)) { + if (variableName && !allVariables.has(variableName) && !iconVariable.test(variableName)) { report(variableName); } } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index b291974cbd9..d06420ae2c4 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -11,12 +11,12 @@ "--vscode-activityBar-inactiveForeground", "--vscode-activityBarBadge-background", "--vscode-activityBarBadge-foreground", + "--vscode-activityBarTop-activeBackground", "--vscode-activityBarTop-activeBorder", + "--vscode-activityBarTop-background", "--vscode-activityBarTop-dropBorder", "--vscode-activityBarTop-foreground", "--vscode-activityBarTop-inactiveForeground", - "--vscode-activityBarTop-background", - "--vscode-activityBarTop-activeBackground", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", @@ -49,7 +49,6 @@ "--vscode-chat-requestBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", - "--vscode-chat-list-background", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-foreground", @@ -95,6 +94,7 @@ "--vscode-debugTokenExpression-name", "--vscode-debugTokenExpression-number", "--vscode-debugTokenExpression-string", + "--vscode-debugTokenExpression-type", "--vscode-debugTokenExpression-value", "--vscode-debugToolBar-background", "--vscode-debugToolBar-border", @@ -130,12 +130,15 @@ "--vscode-editor-background", "--vscode-editor-findMatchBackground", "--vscode-editor-findMatchBorder", + "--vscode-editor-findMatchForeground", "--vscode-editor-findMatchHighlightBackground", "--vscode-editor-findMatchHighlightBorder", + "--vscode-editor-findMatchHighlightForeground", "--vscode-editor-findRangeHighlightBackground", "--vscode-editor-findRangeHighlightBorder", "--vscode-editor-focusedStackFrameHighlightBackground", "--vscode-editor-foldBackground", + "--vscode-editor-foldPlaceholderForeground", "--vscode-editor-foreground", "--vscode-editor-hoverHighlightBackground", "--vscode-editor-inactiveSelectionBackground", @@ -144,6 +147,7 @@ "--vscode-editor-lineHighlightBackground", "--vscode-editor-lineHighlightBorder", "--vscode-editor-linkedEditingBackground", + "--vscode-editor-placeholder-foreground", "--vscode-editor-rangeHighlightBackground", "--vscode-editor-rangeHighlightBorder", "--vscode-editor-selectionBackground", @@ -262,6 +266,10 @@ "--vscode-editorMarkerNavigationInfo-headerBackground", "--vscode-editorMarkerNavigationWarning-background", "--vscode-editorMarkerNavigationWarning-headerBackground", + "--vscode-editorMultiCursor-primary-background", + "--vscode-editorMultiCursor-primary-foreground", + "--vscode-editorMultiCursor-secondary-background", + "--vscode-editorMultiCursor-secondary-foreground", "--vscode-editorOverviewRuler-addedForeground", "--vscode-editorOverviewRuler-background", "--vscode-editorOverviewRuler-border", @@ -331,7 +339,7 @@ "--vscode-icon-foreground", "--vscode-inlineChat-background", "--vscode-inlineChat-border", - "--vscode-inlineChat-regionHighlight", + "--vscode-inlineChat-foreground", "--vscode-inlineChat-shadow", "--vscode-inlineChatDiff-inserted", "--vscode-inlineChatDiff-removed", @@ -485,6 +493,9 @@ "--vscode-panelSectionHeader-background", "--vscode-panelSectionHeader-border", "--vscode-panelSectionHeader-foreground", + "--vscode-panelStickyScroll-background", + "--vscode-panelStickyScroll-border", + "--vscode-panelStickyScroll-shadow", "--vscode-panelTitle-activeBorder", "--vscode-panelTitle-activeForeground", "--vscode-panelTitle-inactiveForeground", @@ -511,6 +522,7 @@ "--vscode-problemsWarningIcon-foreground", "--vscode-profileBadge-background", "--vscode-profileBadge-foreground", + "--vscode-profiles-sashBorder", "--vscode-progressBar-background", "--vscode-quickInput-background", "--vscode-quickInput-foreground", @@ -519,7 +531,21 @@ "--vscode-quickInputList-focusForeground", "--vscode-quickInputList-focusIconForeground", "--vscode-quickInputTitle-background", + "--vscode-radio-activeBackground", + "--vscode-radio-activeBorder", + "--vscode-radio-activeForeground", + "--vscode-radio-inactiveBackground", + "--vscode-radio-inactiveBorder", + "--vscode-radio-inactiveForeground", + "--vscode-radio-inactiveHoverBackground", "--vscode-sash-hoverBorder", + "--vscode-scm-historyGraph-green", + "--vscode-scm-historyGraph-historyItemGroupBase", + "--vscode-scm-historyGraph-historyItemGroupHoverLabelForeground", + "--vscode-scm-historyGraph-historyItemGroupLocal", + "--vscode-scm-historyGraph-historyItemGroupRemote", + "--vscode-scm-historyGraph-red", + "--vscode-scm-historyGraph-yellow", "--vscode-scm-historyItemAdditionsForeground", "--vscode-scm-historyItemDeletionsForeground", "--vscode-scm-historyItemSelectedStatisticsBorder", @@ -558,11 +584,15 @@ "--vscode-sideBar-border", "--vscode-sideBar-dropBackground", "--vscode-sideBar-foreground", + "--vscode-sideBarActivityBarTop-border", "--vscode-sideBarSectionHeader-background", "--vscode-sideBarSectionHeader-border", "--vscode-sideBarSectionHeader-foreground", + "--vscode-sideBarStickyScroll-background", + "--vscode-sideBarStickyScroll-border", + "--vscode-sideBarStickyScroll-shadow", + "--vscode-sideBarTitle-background", "--vscode-sideBarTitle-foreground", - "--vscode-sideBarActivityBarTop-border", "--vscode-sideBySideEditor-horizontalBorder", "--vscode-sideBySideEditor-verticalBorder", "--vscode-simpleFindWidget-sashBorder", @@ -648,6 +678,9 @@ "--vscode-tab-inactiveForeground", "--vscode-tab-inactiveModifiedBorder", "--vscode-tab-lastPinnedBorder", + "--vscode-tab-selectedBackground", + "--vscode-tab-selectedBorderTop", + "--vscode-tab-selectedForeground", "--vscode-tab-unfocusedActiveBackground", "--vscode-tab-unfocusedActiveBorder", "--vscode-tab-unfocusedActiveBorderTop", @@ -685,17 +718,21 @@ "--vscode-terminal-foreground", "--vscode-terminal-hoverHighlightBackground", "--vscode-terminal-inactiveSelectionBackground", + "--vscode-terminal-initialHintForeground", "--vscode-terminal-selectionBackground", "--vscode-terminal-selectionForeground", "--vscode-terminal-tab-activeBorder", "--vscode-terminalCommandDecoration-defaultBackground", "--vscode-terminalCommandDecoration-errorBackground", "--vscode-terminalCommandDecoration-successBackground", + "--vscode-terminalCommandGuide-foreground", "--vscode-terminalCursor-background", "--vscode-terminalCursor-foreground", + "--vscode-terminalOverviewRuler-border", "--vscode-terminalOverviewRuler-cursorForeground", "--vscode-terminalOverviewRuler-findMatchForeground", "--vscode-terminalStickyScroll-background", + "--vscode-terminalStickyScroll-border", "--vscode-terminalStickyScrollHover-background", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", @@ -703,11 +740,17 @@ "--vscode-testing-coveredBorder", "--vscode-testing-coveredGutterBackground", "--vscode-testing-iconErrored", + "--vscode-testing-iconErrored-retired", "--vscode-testing-iconFailed", + "--vscode-testing-iconFailed-retired", "--vscode-testing-iconPassed", + "--vscode-testing-iconPassed-retired", "--vscode-testing-iconQueued", + "--vscode-testing-iconQueued-retired", "--vscode-testing-iconSkipped", + "--vscode-testing-iconSkipped-retired", "--vscode-testing-iconUnset", + "--vscode-testing-iconUnset-retired", "--vscode-testing-message-error-decorationForeground", "--vscode-testing-message-error-lineBackground", "--vscode-testing-message-info-decorationForeground", @@ -721,8 +764,6 @@ "--vscode-testing-uncoveredBorder", "--vscode-testing-uncoveredBranchBackground", "--vscode-testing-uncoveredGutterBackground", - "--vscode-testing-uncoveredGutterBackground", - "--vscode-testing-coverage-lineHeight", "--vscode-textBlockQuote-background", "--vscode-textBlockQuote-border", "--vscode-textCodeBlock-background", @@ -786,6 +827,7 @@ "--testMessageDecorationFontFamily", "--testMessageDecorationFontSize", "--title-border-bottom-color", + "--vscode-chat-list-background", "--vscode-editorCodeLens-fontFamily", "--vscode-editorCodeLens-fontFamilyDefault", "--vscode-editorCodeLens-fontFeatureSettings", @@ -795,8 +837,6 @@ "--vscode-hover-maxWidth", "--vscode-hover-sourceWhiteSpace", "--vscode-hover-whiteSpace", - "--vscode-inline-chat-quick-voice-height", - "--vscode-inline-chat-quick-voice-width", "--vscode-editor-dictation-widget-height", "--vscode-editor-dictation-widget-width", "--vscode-interactive-session-foreground", @@ -807,9 +847,12 @@ "--vscode-repl-line-height", "--vscode-sash-hover-size", "--vscode-sash-size", + "--vscode-testing-coverage-lineHeight", "--vscode-editorStickyScroll-scrollableWidth", "--vscode-editorStickyScroll-foldingOpacityTransition", "--window-border-color", + "--vscode-parameterHintsWidget-editorFontFamily", + "--vscode-parameterHintsWidget-editorFontFamilyDefault", "--workspace-trust-check-color", "--workspace-trust-selected-color", "--workspace-trust-unselected-color", @@ -829,6 +872,8 @@ "--z-index-notebook-scrollbar", "--z-index-run-button-container", "--zoom-factor", - "--test-bar-width" + "--test-bar-width", + "--widget-color", + "--text-link-decoration" ] -} +} \ No newline at end of file diff --git a/build/lib/tsb/transpiler.js b/build/lib/tsb/transpiler.js index afec9062692..5dcc4ca1ed3 100644 --- a/build/lib/tsb/transpiler.js +++ b/build/lib/tsb/transpiler.js @@ -305,7 +305,7 @@ class SwcTranspiler { }, module: { type: 'amd', - noInterop: true + noInterop: false }, minify: false, }; @@ -313,7 +313,7 @@ class SwcTranspiler { ...this._swcrcAmd, module: { type: 'commonjs', - importInterop: 'none' + importInterop: 'swc' } }; static _swcrcEsm = { diff --git a/build/lib/tsb/transpiler.ts b/build/lib/tsb/transpiler.ts index a546ea63316..cbc3d9e8eee 100644 --- a/build/lib/tsb/transpiler.ts +++ b/build/lib/tsb/transpiler.ts @@ -388,7 +388,7 @@ export class SwcTranspiler implements ITranspiler { }, module: { type: 'amd', - noInterop: true + noInterop: false }, minify: false, }; @@ -397,7 +397,7 @@ export class SwcTranspiler implements ITranspiler { ...this._swcrcAmd, module: { type: 'commonjs', - importInterop: 'none' + importInterop: 'swc' } }; diff --git a/build/lib/util.js b/build/lib/util.js index ed52776c2c0..02ce049b00b 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -34,7 +34,6 @@ const rename = require("gulp-rename"); const path = require("path"); const fs = require("fs"); const _rimraf = require("rimraf"); -const VinylFile = require("vinyl"); const url_1 = require("url"); const ternaryStream = require("ternary-stream"); const root = path.dirname(path.dirname(__dirname)); diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js index 49094e915e4..934d8e8110f 100644 --- a/build/lib/watch/watch-win32.js +++ b/build/lib/watch/watch-win32.js @@ -70,8 +70,8 @@ module.exports = function (pattern, options) { return f; }); return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) + .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe(filter(pattern, { dot: options.dot })) .pipe(es.map(function (file, cb) { fs.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index fa65a5bdeb2..afde6a79f22 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -70,7 +70,7 @@ function watch(root: string): Stream { const cache: { [cwd: string]: Stream } = Object.create(null); -module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string }) { +module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string; dot?: boolean }) { options = options || {}; const cwd = path.normalize(options.cwd || process.cwd()); @@ -86,8 +86,8 @@ module.exports = function (pattern: string | string[] | filter.FileFunction, opt }); return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) + .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe(filter(pattern, { dot: options.dot })) .pipe(es.map(function (file: File, cb) { fs.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { return cb(undefined, file); } diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index bdb265b6fec..3a642a72725 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -31,6 +31,7 @@ exports.referenceGeneratedDepsByArch = { 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', 'libc6 (>= 2.2.5)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -57,8 +58,7 @@ exports.referenceGeneratedDepsByArch = { 'libxkbcommon0 (>= 0.5.0)', 'libxkbfile1 (>= 1:1.1.0)', 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' + 'xdg-utils (>= 1.0.2)' ], 'armhf': [ 'ca-certificates', @@ -68,6 +68,7 @@ exports.referenceGeneratedDepsByArch = { 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libc6 (>= 2.4)', 'libc6 (>= 2.9)', @@ -109,6 +110,7 @@ exports.referenceGeneratedDepsByArch = { 'libatk1.0-0 (>= 2.2.0)', 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 3d6c2eba6e9..86d1de12216 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -31,6 +31,7 @@ export const referenceGeneratedDepsByArch = { 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', 'libc6 (>= 2.2.5)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -57,8 +58,7 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon0 (>= 0.5.0)', 'libxkbfile1 (>= 1:1.1.0)', 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' + 'xdg-utils (>= 1.0.2)' ], 'armhf': [ 'ca-certificates', @@ -68,6 +68,7 @@ export const referenceGeneratedDepsByArch = { 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libc6 (>= 2.4)', 'libc6 (>= 2.9)', @@ -109,6 +110,7 @@ export const referenceGeneratedDepsByArch = { 'libatk1.0-0 (>= 2.2.0)', 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 80c247d1129..19adbeb0529 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -23,7 +23,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/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243: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 9f1a068b8d7..5fe4ac5da64 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/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243: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 bd84fc146dc..97984514511 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -38,17 +38,21 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.10)(64bit)', 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.12)(64bit)', 'libc.so.6(GLIBC_2.14)(64bit)', 'libc.so.6(GLIBC_2.15)(64bit)', 'libc.so.6(GLIBC_2.16)(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', 'libc.so.6(GLIBC_2.3.3)(64bit)', 'libc.so.6(GLIBC_2.3.4)(64bit)', 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.5)(64bit)', 'libc.so.6(GLIBC_2.6)(64bit)', 'libc.so.6(GLIBC_2.7)(64bit)', 'libc.so.6(GLIBC_2.8)(64bit)', @@ -109,8 +113,6 @@ exports.referenceGeneratedDepsByArch = { 'libxkbcommon.so.0()(64bit)', 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', - 'libz.so.1()(64bit)', - 'libz.so.1(ZLIB_1.2.3.4)(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', 'rtld(GNU_HASH)', 'xdg-utils' @@ -134,11 +136,16 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6', 'libc.so.6(GLIBC_2.10)', 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.12)', + 'libc.so.6(GLIBC_2.14)', 'libc.so.6(GLIBC_2.15)', 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.18)', + 'libc.so.6(GLIBC_2.25)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.5)', 'libc.so.6(GLIBC_2.6)', 'libc.so.6(GLIBC_2.7)', 'libc.so.6(GLIBC_2.8)', @@ -237,6 +244,8 @@ exports.referenceGeneratedDepsByArch = { 'libatspi.so.0()(64bit)', 'libc.so.6()(64bit)', '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.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 82a4fe7698d..b79812784bf 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -37,17 +37,21 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.10)(64bit)', 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.12)(64bit)', 'libc.so.6(GLIBC_2.14)(64bit)', 'libc.so.6(GLIBC_2.15)(64bit)', 'libc.so.6(GLIBC_2.16)(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', 'libc.so.6(GLIBC_2.3.3)(64bit)', 'libc.so.6(GLIBC_2.3.4)(64bit)', 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.5)(64bit)', 'libc.so.6(GLIBC_2.6)(64bit)', 'libc.so.6(GLIBC_2.7)(64bit)', 'libc.so.6(GLIBC_2.8)(64bit)', @@ -108,8 +112,6 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon.so.0()(64bit)', 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', - 'libz.so.1()(64bit)', - 'libz.so.1(ZLIB_1.2.3.4)(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', 'rtld(GNU_HASH)', 'xdg-utils' @@ -133,11 +135,16 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6', 'libc.so.6(GLIBC_2.10)', 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.12)', + 'libc.so.6(GLIBC_2.14)', 'libc.so.6(GLIBC_2.15)', 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.18)', + 'libc.so.6(GLIBC_2.25)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.5)', 'libc.so.6(GLIBC_2.6)', 'libc.so.6(GLIBC_2.7)', 'libc.so.6(GLIBC_2.8)', @@ -236,6 +243,8 @@ export const referenceGeneratedDepsByArch = { 'libatspi.so.0()(64bit)', 'libc.so.6()(64bit)', '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.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/npm/dirs.js b/build/npm/dirs.js index faf3a6577a5..fbefd418b0a 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -29,7 +29,6 @@ const dirs = [ 'extensions/jake', 'extensions/json-language-features', 'extensions/json-language-features/server', - 'extensions/markdown-language-features/server', 'extensions/markdown-language-features', 'extensions/markdown-math', 'extensions/media-preview', @@ -52,6 +51,7 @@ const dirs = [ 'test/integration/browser', 'test/monaco', 'test/smoke', + '.vscode/extensions/vscode-selfhost-test-provider', ]; if (fs.existsSync(`${__dirname}/../../.build/distro/npm`)) { diff --git a/build/npm/gyp/package.json b/build/npm/gyp/package.json index 3961e955a5f..a1564133a1e 100644 --- a/build/npm/gyp/package.json +++ b/build/npm/gyp/package.json @@ -4,7 +4,7 @@ "private": true, "license": "MIT", "devDependencies": { - "node-gyp": "^9.4.0" + "node-gyp": "^10.1.0" }, "scripts": {} } diff --git a/build/npm/gyp/yarn.lock b/build/npm/gyp/yarn.lock index c444d89094f..a9bf901727e 100644 --- a/build/npm/gyp/yarn.lock +++ b/build/npm/gyp/yarn.lock @@ -14,10 +14,21 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@npmcli/agent@^2.0.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-2.2.2.tgz#967604918e62f620a648c7975461c9c9e74fc5d5" + integrity sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og== + dependencies: + agent-base "^7.1.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.1" + lru-cache "^10.0.1" + socks-proxy-agent "^8.0.3" + "@npmcli/fs@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.0.tgz#233d43a25a91d68c3a863ba0da6a3f00924a173e" - integrity sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w== + version "3.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.1.tgz#59cdaa5adca95d135fc00f2bb53f5771575ce726" + integrity sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg== dependencies: semver "^7.3.5" @@ -26,31 +37,17 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@tootallnate/once@2": +abbrev@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== -abbrev@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -agent-base@6, agent-base@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== +agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== dependencies: - debug "4" - -agentkeepalive@^4.2.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" - integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== - dependencies: - debug "^4.1.0" - depd "^2.0.0" - humanize-ms "^1.2.1" + debug "^4.3.4" aggregate-error@^3.0.0: version "3.1.0" @@ -82,32 +79,11 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - brace-expansion@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" @@ -115,17 +91,17 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -cacache@^17.0.0: - version "17.1.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.3.tgz#c6ac23bec56516a7c0c52020fd48b4909d7c7044" - integrity sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg== +cacache@^18.0.0: + version "18.0.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-18.0.3.tgz#864e2c18414e1e141ae8763f31e46c2cb96d1b21" + integrity sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg== dependencies: "@npmcli/fs" "^3.1.0" fs-minipass "^3.0.0" glob "^10.2.2" - lru-cache "^7.7.1" - minipass "^5.0.0" - minipass-collect "^1.0.2" + lru-cache "^10.0.1" + minipass "^7.0.3" + minipass-collect "^2.0.1" minipass-flush "^1.0.5" minipass-pipeline "^1.2.4" p-map "^4.0.0" @@ -155,21 +131,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - cross-spawn@^7.0.0: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -179,22 +140,19 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" -debug@4, debug@^4.1.0, debug@^4.3.3: +debug@4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -depd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" eastasianwidth@^0.2.0: version "0.2.0" @@ -234,9 +192,9 @@ exponential-backoff@^3.1.1: integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" @@ -249,93 +207,50 @@ fs-minipass@^2.0.0: minipass "^3.0.0" fs-minipass@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.2.tgz#5b383858efa8c1eb8c33b39e994f7e8555b8b3a3" - integrity sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g== + version "3.0.3" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" + integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - -glob@^10.2.2: - version "10.3.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b" - integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw== +glob@^10.2.2, glob@^10.3.10: + version "10.4.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" + integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== dependencies: foreground-child "^3.1.0" - jackspeak "^2.0.3" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" graceful-fs@^4.2.6: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" + agent-base "^7.1.0" + debug "^4.3.4" -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== +https-proxy-agent@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== dependencies: - agent-base "6" + agent-base "^7.0.2" debug "4" -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -353,23 +268,13 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ip@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" - integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== + jsbn "1.1.0" + sprintf-js "^1.1.3" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -386,15 +291,30 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jackspeak@^2.0.3: - version "2.2.2" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.2.tgz#707c62733924b8dc2a0a629dc6248577788b5385" - integrity sha512-mgNtVv4vUuaKA97yxUHoA3+FkuhtxkjdXEWOyB/N76fjy0FjezEt34oy3epBtvCvS+7DyKwqCFWx/oJLV5+kCg== +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== + +jackspeak@^3.1.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" + integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +lru-cache@^10.0.1, lru-cache@^10.2.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.3.0.tgz#4a4aaf10c84658ab70f79a85a9a3f1e1fb11196b" + integrity sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -402,64 +322,44 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.7.1: - version "7.18.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -"lru-cache@^9.1.1 || ^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" - integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== - -make-fetch-happen@^11.0.3: - version "11.1.1" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" - integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== +make-fetch-happen@^13.0.0: + version "13.0.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz#273ba2f78f45e1f3a6dca91cede87d9fa4821e36" + integrity sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA== dependencies: - agentkeepalive "^4.2.1" - cacache "^17.0.0" + "@npmcli/agent" "^2.0.0" + cacache "^18.0.0" http-cache-semantics "^4.1.1" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^5.0.0" + minipass "^7.0.2" minipass-fetch "^3.0.0" minipass-flush "^1.0.5" minipass-pipeline "^1.2.4" negotiator "^0.6.3" + proc-log "^4.2.0" promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" ssri "^10.0.0" -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== +minipass-collect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-2.0.1.tgz#1621bc77e12258a12c60d34e2276ec5c20680863" + integrity sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw== dependencies: - minipass "^3.0.0" + minipass "^7.0.3" minipass-fetch@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.3.tgz#d9df70085609864331b533c960fd4ffaa78d15ce" - integrity sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ== + version "3.0.5" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.5.tgz#f0f97e40580affc4a35cc4a1349f05ae36cb1e4c" + integrity sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" minipass-sized "^1.0.3" minizlib "^2.1.2" optionalDependencies: @@ -493,20 +393,15 @@ minipass@^3.0.0: dependencies: yallist "^4.0.0" -minipass@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.3.tgz#00bfbaf1e16e35e804f4aa31a7c1f6b8d9f0ee72" - integrity sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw== - minipass@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e" - integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" @@ -526,56 +421,33 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -node-gyp@^9.4.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.0.tgz#2a7a91c7cba4eccfd95e949369f27c9ba704f369" - integrity sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg== +node-gyp@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-10.1.0.tgz#75e6f223f2acb4026866c26a2ead6aab75a8ca7e" + integrity sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA== dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1" - glob "^7.1.4" + glob "^10.3.10" graceful-fs "^4.2.6" - make-fetch-happen "^11.0.3" - nopt "^6.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" + make-fetch-happen "^13.0.0" + nopt "^7.0.0" + proc-log "^3.0.0" semver "^7.3.5" tar "^6.1.2" - which "^2.0.2" + which "^4.0.0" -nopt@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" - integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== +nopt@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== dependencies: - abbrev "^1.0.0" - -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" + abbrev "^2.0.0" p-map@^4.0.0: version "4.0.0" @@ -584,24 +456,34 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +proc-log@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" + integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== + +proc-log@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-4.2.0.tgz#b6f461e4026e75fdfe228b265e9f7a00779d7034" + integrity sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA== + promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -610,32 +492,11 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -648,11 +509,6 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -665,11 +521,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -680,31 +531,36 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -socks-proxy-agent@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" - integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== +socks-proxy-agent@^8.0.3: + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.8.3" -socks@^2.6.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" -ssri@^10.0.0: - version "10.0.4" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.4.tgz#5a20af378be586df139ddb2dfb3bf992cf0daba6" - integrity sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ== - dependencies: - minipass "^5.0.0" +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: +ssri@^10.0.0: + version "10.0.6" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" + integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== + dependencies: + minipass "^7.0.3" + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -722,14 +578,8 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -743,10 +593,10 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" -tar@^6.1.11: - version "6.1.15" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69" - integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A== +tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -755,18 +605,6 @@ tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.1.2: - version "6.1.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^4.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - unique-filename@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" @@ -781,24 +619,19 @@ unique-slug@^4.0.0: dependencies: imurmurhash "^0.1.4" -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== +which@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" + integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== dependencies: - string-width "^1.0.2 || 2 || 3 || 4" + isexe "^3.1.1" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" @@ -818,11 +651,6 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 72dd74f8986..d45d5bc8cbc 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -36,6 +36,7 @@ function yarnInstall(dir, opts) { ...(opts ?? {}), cwd: dir, stdio: 'inherit', + shell: true }; const raw = process.env['npm_config_argv'] || '{}'; @@ -53,14 +54,10 @@ function yarnInstall(dir, opts) { console.log(`Installing dependencies in ${dir} inside container ${process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME']}...`); opts.cwd = root; - if (process.env['npm_config_arch'] === 'arm64' || process.env['npm_config_arch'] === 'arm') { + if (process.env['npm_config_arch'] === 'arm64') { run('sudo', ['docker', 'run', '--rm', '--privileged', 'multiarch/qemu-user-static', '--reset', '-p', 'yes'], opts); } - if (process.env['npm_config_arch'] === 'arm') { - run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/home/builduser`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/home/builduser/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); - } else { - run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); - } + run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); run('sudo', ['chown', '-R', `${userinfo.uid}:${userinfo.gid}`, `${dir}/node_modules`], opts); } else { console.log(`Installing dependencies in ${dir}...`); diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index edf0d98c3d5..7c51e83ff9c 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -10,13 +10,10 @@ const minorNodeVersion = parseInt(nodeVersion[2]); const patchNodeVersion = parseInt(nodeVersion[3]); if (!process.env['VSCODE_SKIP_NODE_VERSION_CHECK']) { - if (majorNodeVersion < 18 || (majorNodeVersion === 18 && minorNodeVersion < 15)) { - console.error('\x1b[1;31m*** Please use node.js versions >=18.15.x and <19.\x1b[0;0m'); + if (majorNodeVersion < 20) { + console.error('\x1b[1;31m*** Please use latest Node.js v20 LTS for development.\x1b[0;0m'); err = true; } - if (majorNodeVersion >= 19) { - console.warn('\x1b[1;31m*** Warning: Versions of node.js >= 19 have not been tested.\x1b[0;0m') - } } const path = require('path'); @@ -76,9 +73,9 @@ function hasSupportedVisualStudioVersion() { const programFiles86Path = process.env['ProgramFiles(x86)']; const programFiles64Path = process.env['ProgramFiles']; + const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools', 'IntPreview']; if (programFiles64Path) { vsPath = `${programFiles64Path}/Microsoft Visual Studio/${version}`; - const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools']; if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { availableVersions.push(version); break; @@ -87,7 +84,6 @@ function hasSupportedVisualStudioVersion() { if (programFiles86Path) { vsPath = `${programFiles86Path}/Microsoft Visual Studio/${version}`; - const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools']; if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { availableVersions.push(version); break; @@ -102,7 +98,8 @@ function installHeaders() { const yarnResult = cp.spawnSync(yarn, ['install'], { env: process.env, cwd: path.join(__dirname, 'gyp'), - stdio: 'inherit' + stdio: 'inherit', + shell: true }); if (yarnResult.error || yarnResult.status !== 0) { console.error(`Installing node-gyp failed`); @@ -114,7 +111,7 @@ function installHeaders() { // file checked into our repository. So from that point it is save to construct the path // to that executable const node_gyp = path.join(__dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd'); - const result = cp.execFileSync(node_gyp, ['list'], { encoding: 'utf8' }); + const result = cp.execFileSync(node_gyp, ['list'], { encoding: 'utf8', shell: true }); const versions = new Set(result.split(/\n/g).filter(line => !line.startsWith('gyp info')).map(value => value)); const local = getHeaderInfo(path.join(__dirname, '..', '..', '.yarnrc')); @@ -122,7 +119,7 @@ function installHeaders() { if (local !== undefined && !versions.has(local.target)) { // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target]); + cp.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target], { shell: true }); } // Avoid downloading headers for Windows arm64 till we move to Nodejs v19 in remote @@ -139,7 +136,7 @@ function installHeaders() { process.env['npm_config_arch'] !== "arm64" && process.arch !== "arm64") { // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target]); + cp.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target], { shell: true }); } } diff --git a/build/package.json b/build/package.json index b4ea8d06993..42e67a02b0d 100644 --- a/build/package.json +++ b/build/package.json @@ -4,7 +4,7 @@ "license": "MIT", "devDependencies": { "@azure/cosmos": "^3", - "@azure/identity": "^3.4.1", + "@azure/identity": "^4.2.1", "@azure/storage-blob": "^12.17.0", "@electron/get": "^2.0.0", "@types/ansi-colors": "^3.2.0", @@ -14,20 +14,20 @@ "@types/fancy-log": "^1.3.0", "@types/fs-extra": "^9.0.12", "@types/glob": "^7.1.1", - "@types/gulp": "^4.0.5", + "@types/gulp": "^4.0.17", "@types/gulp-concat": "^0.0.32", "@types/gulp-filter": "^3.0.32", "@types/gulp-gzip": "^0.0.31", "@types/gulp-json-editor": "^2.2.31", - "@types/gulp-postcss": "^8.0.6", "@types/gulp-rename": "^0.0.33", + "@types/gulp-sort": "^2.0.4", "@types/gulp-sourcemaps": "^0.0.32", "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/pump": "^1.0.1", "@types/rimraf": "^2.0.4", "@types/through": "^0.0.29", @@ -42,10 +42,10 @@ "commander": "^7.0.0", "debug": "^4.3.2", "electron-osx-sign": "^0.4.16", - "esbuild": "0.20.0", + "esbuild": "0.23.0", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", - "gulp-shell": "^0.8.0", + "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", "mkdirp": "^1.0.4", @@ -53,10 +53,11 @@ "ternary-stream": "^3.0.0", "through2": "^4.0.2", "tmp": "^0.2.1", - "vscode-universal-bundler": "^0.0.2", + "vscode-universal-bundler": "^0.1.0", "workerpool": "^6.4.0", "yauzl": "^2.10.0" }, + "type": "commonjs", "scripts": { "compile": "../node_modules/.bin/tsc -p tsconfig.build.json", "watch": "../node_modules/.bin/tsc -p tsconfig.build.json --watch", @@ -64,7 +65,7 @@ }, "optionalDependencies": { "tree-sitter": "^0.20.5", - "tree-sitter-typescript": "^0.20.3", + "tree-sitter-typescript": "^0.20.5", "vscode-gulp-watch": "^5.0.3" } } diff --git a/build/tsconfig.build.json b/build/tsconfig.build.json index 801c7735b06..4534420208f 100644 --- a/build/tsconfig.build.json +++ b/build/tsconfig.build.json @@ -3,7 +3,8 @@ "compilerOptions": { "allowJs": false, "checkJs": false, - "noEmit": false + "noEmit": false, + "skipLibCheck": true }, "include": [ "**/*.ts" diff --git a/build/win32/Cargo.lock b/build/win32/Cargo.lock index 18edefc752d..8d317f7cab0 100644 --- a/build/win32/Cargo.lock +++ b/build/win32/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -100,23 +89,31 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "inno_updater" -version = "0.11.0" +version = "0.11.1" dependencies = [ "byteorder", "crc", "slog", "slog-async", "slog-term", - "windows-sys", + "windows-sys 0.42.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", ] [[package]] @@ -210,11 +207,11 @@ dependencies = [ [[package]] name = "slog-term" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87d29185c55b7b258b4f120eab00f48557d4d9bc814f41713f449d35b0f8977c" +checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" dependencies = [ - "atty", + "is-terminal", "slog", "term", "thread_local", @@ -336,13 +333,38 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -351,38 +373,86 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/build/win32/Cargo.toml b/build/win32/Cargo.toml index 3925505c225..9dd72608934 100644 --- a/build/win32/Cargo.toml +++ b/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.11.0" +version = "0.11.1" authors = ["Microsoft "] build = "build.rs" @@ -9,7 +9,7 @@ byteorder = "1.4.3" crc = "3.0.1" slog = "2.7.0" slog-async = "2.7.0" -slog-term = "2.9.0" +slog-term = "2.9.1" [target.'cfg(windows)'.dependencies.windows-sys] version = "0.42" diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index b87cbd47f24..5f969cc326c 100644 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/build/yarn.lock b/build/yarn.lock index a0aca2e9595..70a53f30bf8 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -9,20 +9,19 @@ dependencies: tslib "^2.0.0" +"@azure/abort-controller@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" + integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== + dependencies: + tslib "^2.6.2" + "@azure/core-asynciterator-polyfill@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz#dcccebb88406e5c76e0e1d52e8cc4c43a68b3ee7" integrity sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg== -"@azure/core-auth@^1.3.0": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.3.2.tgz#6a2c248576c26df365f6c7881ca04b7f6d08e3d0" - integrity sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA== - dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" - -"@azure/core-auth@^1.5.0": +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.5.0.tgz#a41848c5c31cb3b7c84c409885267d55a2c92e44" integrity sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw== @@ -81,22 +80,7 @@ dependencies: "@azure/core-asynciterator-polyfill" "^1.0.0" -"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.2.0": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.3.2.tgz#82bfb4e960b4ecf4f1a1cdb1afde4ce9192aef09" - integrity sha512-kymICKESeHBpVLgQiAxllgWdSTopkqtmfPac8ITwMCxNEC6hzbSpqApYbjzxbBNkBMgoD4GESo6LLhR/sPh6kA== - dependencies: - "@azure/abort-controller" "^1.0.0" - "@azure/core-auth" "^1.3.0" - "@azure/core-tracing" "1.0.0-preview.13" - "@azure/logger" "^1.0.0" - form-data "^4.0.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - tslib "^2.2.0" - uuid "^8.3.0" - -"@azure/core-rest-pipeline@^1.5.0": +"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.2.0", "@azure/core-rest-pipeline@^1.5.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.7.0.tgz#71f42c19af160422cc84513809ff9668d8047087" integrity sha512-e2awPzwMKHrmvYgZ0qIKNkqnCM1QoDs7A0rOiS3OSAlOQOz/kL7PPKHXwFMuBeaRvS8i7fgobJn79q2Cji5f+Q== @@ -126,21 +110,13 @@ dependencies: tslib "^2.2.0" -"@azure/core-util@^1.1.0", "@azure/core-util@^1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.6.1.tgz#fea221c4fa43c26543bccf799beb30c1c7878f5a" - integrity sha512-h5taHeySlsV9qxuK64KZxy4iln1BtMYlNt5jbuEFN3UFSAd1EwKg/Gjl5a6tZ/W8t6li3xPnutOx7zbDyXnPmQ== +"@azure/core-util@^1.1.0", "@azure/core-util@^1.1.1", "@azure/core-util@^1.3.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.0.tgz#469afd7e6452d5388b189f90d33f7756b0b210d1" + integrity sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw== dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" - -"@azure/core-util@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.2.0.tgz#3499deba1fc36dda6f1912b791809b6f15d4a392" - integrity sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng== - dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" "@azure/cosmos@^3": version "3.17.3" @@ -161,20 +137,20 @@ universal-user-agent "^6.0.0" uuid "^8.3.0" -"@azure/identity@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-3.4.1.tgz#18ba48b7421c818ef8116e8eec3c03ec1a62649a" - integrity sha512-oQ/r5MBdfZTMIUcY5Ch8G7Vv9aIXDkEYyU4Dfqjim4MQN+LY2uiQ57P1JDopMLeHCsZxM4yy8lEdne3tM9Xhzg== +"@azure/identity@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.2.1.tgz#22b366201e989b7b41c0e1690e103bd579c31e4c" + integrity sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q== dependencies: "@azure/abort-controller" "^1.0.0" "@azure/core-auth" "^1.5.0" "@azure/core-client" "^1.4.0" "@azure/core-rest-pipeline" "^1.1.0" "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.6.1" + "@azure/core-util" "^1.3.0" "@azure/logger" "^1.0.0" - "@azure/msal-browser" "^3.5.0" - "@azure/msal-node" "^2.5.1" + "@azure/msal-browser" "^3.11.1" + "@azure/msal-node" "^2.9.2" events "^3.0.0" jws "^4.0.0" open "^8.0.0" @@ -188,24 +164,24 @@ dependencies: tslib "^2.0.0" -"@azure/msal-browser@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.5.0.tgz#eb64c931c78c2b75c70807f618e1284bbb183380" - integrity sha512-2NtMuel4CI3UEelCPKkNRXgKzpWEX48fvxIvPz7s0/sTcCaI08r05IOkH2GkXW+czUOtuY6+oGafJCpumnjRLg== +"@azure/msal-browser@^3.11.1": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.17.0.tgz#dee9ccae586239e7e0708b261f7ffa5bc7e00fb7" + integrity sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A== dependencies: - "@azure/msal-common" "14.4.0" + "@azure/msal-common" "14.12.0" -"@azure/msal-common@14.4.0": - version "14.4.0" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.4.0.tgz#f938c1d96bb73d65baab985c96faaa273c97cfd5" - integrity sha512-ffCymScQuMKVj+YVfwNI52A5Tu+uiZO2eTf+c+3TXxdAssks4nokJhtr+uOOMxH0zDi6d1OjFKFKeXODK0YLSg== +"@azure/msal-common@14.12.0": + version "14.12.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.12.0.tgz#844abe269b071f8fa8949dadc2a7b65bbb147588" + integrity sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw== -"@azure/msal-node@^2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.5.1.tgz#d180a1ba5fdc611a318a8f018a2db3453e2e2898" - integrity sha512-PsPRISqCG253HQk1cAS7eJW7NWTbnBGpG+vcGGz5z4JYRdnM2EIXlj1aBpXCdozenEPtXEVvHn2ELleW1w82nQ== +"@azure/msal-node@^2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.9.2.tgz#e6d3c1661012c1bd0ef68e328f73a2fdede52931" + integrity sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ== dependencies: - "@azure/msal-common" "14.4.0" + "@azure/msal-common" "14.12.0" jsonwebtoken "^9.0.0" uuid "^8.3.0" @@ -223,6 +199,15 @@ events "^3.0.0" tslib "^2.2.0" +"@electron/asar@^3.2.7": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.10.tgz#615cf346b734b23cafa4e0603551010bd0e50aa8" + integrity sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw== + dependencies: + commander "^5.0.0" + glob "^7.1.6" + minimatch "^3.0.4" + "@electron/get@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960" @@ -238,125 +223,130 @@ optionalDependencies: global-agent "^3.0.0" -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" - integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== + +"@malept/cross-spawn-promise@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz#d0772de1aa680a0bfb9ba2f32b4c828c7857cb9d" + integrity sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg== dependencies: cross-spawn "^7.0.1" @@ -404,14 +394,6 @@ "@types/node" "*" "@types/responselike" "^1.0.0" -"@types/chokidar@*": - version "1.7.5" - resolved "https://registry.yarnpkg.com/@types/chokidar/-/chokidar-1.7.5.tgz#1fa78c8803e035bed6d98e6949e514b133b0c9b6" - integrity sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ== - dependencies: - "@types/events" "*" - "@types/node" "*" - "@types/debounce@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.0.0.tgz#417560200331e1bb84d72da85391102c2fcd61b7" @@ -429,6 +411,11 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA== +"@types/expect@^1.20.4": + version "1.20.4" + resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" + integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== + "@types/fancy-log@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.0.tgz#a61ab476e5e628cd07a846330df53b85e05c8ce0" @@ -489,14 +476,6 @@ "@types/js-beautify" "*" "@types/node" "*" -"@types/gulp-postcss@^8.0.6": - version "8.0.6" - resolved "https://registry.yarnpkg.com/@types/gulp-postcss/-/gulp-postcss-8.0.6.tgz#d314c785876c8a1779154ba1d152125082ecde0f" - integrity sha512-mjGEmTvurqRHFeJQnrgtMC9GtKNkI2+56n92zIzff5UFr2jUfilw1elKRxS7bK0FYRvuEcnMX9JH0AUpCxBrpg== - dependencies: - "@types/node" "*" - "@types/vinyl" "*" - "@types/gulp-rename@^0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/gulp-rename/-/gulp-rename-0.0.33.tgz#38d146e97786569f74f5391a1b1f9b5198674b6c" @@ -504,6 +483,14 @@ dependencies: "@types/node" "*" +"@types/gulp-sort@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/gulp-sort/-/gulp-sort-2.0.4.tgz#60625bf608dbac8f03644c6785d25c616f1b7d8c" + integrity sha512-HUHxH+oMox1ct0SnxPqCXBni0MSws1ygcSAoLO4ISRmR/UuvNIz40rgNveZxwxQk+p78kw09z/qKQkgKJmNUOQ== + dependencies: + "@types/gulp-util" "*" + "@types/node" "*" + "@types/gulp-sourcemaps@^0.0.32": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/gulp-sourcemaps/-/gulp-sourcemaps-0.0.32.tgz#e79ee617e0cb15729874be4533fe59c07793a175" @@ -511,14 +498,25 @@ dependencies: "@types/node" "*" -"@types/gulp@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.5.tgz#f5f498d5bf9538364792de22490a12c0e6bc5eb4" - integrity sha512-nx1QjPTiRpvLfYsZ7MBu7bT6Cm7tAXyLbY0xbdx2IEMxCK2v2urIhJMQZHW0iV1TskM71Xl6p2uRRuWDbk+/7g== +"@types/gulp-util@*": + version "3.0.41" + resolved "https://registry.yarnpkg.com/@types/gulp-util/-/gulp-util-3.0.41.tgz#52868a6f8b6af55a099fe48ea20736833c02bed4" + integrity sha512-BK0kJZ8euQNlISsmD6mBr/1RZkB0mljdtBsz2usv+QHPV10alH2AJw5p05S9LU6S+VdTjbFmGU0OxpH++2W9/Q== dependencies: - "@types/chokidar" "*" - "@types/undertaker" "*" + "@types/node" "*" + "@types/through2" "*" + "@types/vinyl" "*" + chalk "^2.2.0" + +"@types/gulp@^4.0.17": + version "4.0.17" + resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.17.tgz#b314c3762d08d8d69b7c0b86f78d069bafd65009" + integrity sha512-+pKQynu2C/HS16kgmDlAicjtFYP8kaa86eE9P0Ae7GB5W29we/E2TIdbOWtEZD5XkpY+jr8fyqfwO6SWZecLpQ== + dependencies: + "@types/node" "*" + "@types/undertaker" ">=1.2.6" "@types/vinyl-fs" "*" + chokidar "^3.3.1" "@types/http-cache-semantics@*": version "4.0.4" @@ -582,10 +580,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@18.x": - version "18.18.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.9.tgz#5527ea1832db3bba8eb8023ce8497b7d3f299592" - integrity sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== dependencies: undici-types "~5.26.4" @@ -611,6 +609,13 @@ "@types/glob" "*" "@types/node" "*" +"@types/through2@*": + version "2.0.41" + resolved "https://registry.yarnpkg.com/@types/through2/-/through2-2.0.41.tgz#3e5e1720d71ffdfa03c22f2aad6593d12a47034f" + integrity sha512-ryQ0tidWkb1O1JuYvWKyMLYEtOWDqF5mHerJzKz/gQpoAaJq2l/dsMPBF0B5BNVT34rbARYJ5/tsZwLfUi2kwQ== + dependencies: + "@types/node" "*" + "@types/through2@^2.0.36": version "2.0.36" resolved "https://registry.yarnpkg.com/@types/through2/-/through2-2.0.36.tgz#35fda0db635827d44c0e08e2c94653e647574a00" @@ -642,13 +647,14 @@ resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da" integrity sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ== -"@types/undertaker@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.0.tgz#d39a81074b4f274eb656870fc904a70737e00f8e" - integrity sha512-bx/5nZCGkasXs6qaA3B6SVDjBZqdyk04UO12e0uEPSzjt5H8jEJw0DKe7O7IM0hM2bVHRh70pmOH7PEHqXwzOw== +"@types/undertaker@>=1.2.6": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.11.tgz#d9e08b72c4bea5fc40e5bad63ad5a1a2b675e3ca" + integrity sha512-j1Z0V2ByRHr8ZK7eOeGq0LGkkdthNFW0uAZGY22iRkNQNL9/vAV0yFPr1QN3FM/peY5bxs9P+1f0PYJTQVa5iA== dependencies: - "@types/events" "*" + "@types/node" "*" "@types/undertaker-registry" "*" + async-done "~1.3.2" "@types/vinyl-fs@*": version "2.4.9" @@ -661,10 +667,11 @@ "@types/vinyl" "*" "@types/vinyl@*": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.2.tgz#4f3b8dae8f5828d3800ef709b0cff488ee852de3" - integrity sha512-2iYpNuOl98SrLPBZfEN9Mh2JCJ2EI9HU35SfgBEb51DcmaHkhp8cKMblYeBqMQiwXMgAD3W60DbQ4i/UdLiXhw== + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.12.tgz#17642ca9a8ae10f3db018e9f885da4188db4c6e6" + integrity sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw== dependencies: + "@types/expect" "^1.20.4" "@types/node" "*" "@types/workerpool@^6.4.0": @@ -718,6 +725,11 @@ optionalDependencies: keytar "^7.7.0" +"@xmldom/xmldom@^0.8.8": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -751,13 +763,6 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -771,6 +776,14 @@ anymatch@^3.1.1, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -786,33 +799,26 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -asar@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" - integrity sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw== - dependencies: - chromium-pickle-js "^0.2.0" - commander "^5.0.0" - glob "^7.1.6" - minimatch "^3.0.4" - optionalDependencies: - "@types/glob" "^7.1.1" - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +async-done@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" + integrity sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.2" + process-nextick-args "^2.0.0" + stream-exhaust "^1.0.1" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - azure-devops-node-api@^11.0.1: version "11.2.0" resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz#bf04edbef60313117a0507415eed4790a420ad6b" @@ -868,12 +874,19 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: - fill-range "^7.0.1" + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -898,11 +911,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-equal@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" - integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= - buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -947,7 +955,7 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.2" -chalk@^2.4.2: +chalk@^2.2.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -956,14 +964,6 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - cheerio-select@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" @@ -1004,16 +1004,26 @@ chokidar@3.5.1: optionalDependencies: fsevents "~2.3.1" +chokidar@^3.3.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -chromium-pickle-js@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" - integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= - clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -1052,33 +1062,16 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= - colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -1091,13 +1084,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= - dependencies: - graceful-readlink ">= 1.0.0" - commander@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" @@ -1211,15 +1197,13 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -dir-compare@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" - integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA== +dir-compare@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-4.2.0.tgz#d1d4999c14fbf55281071fdae4293b3b9ce86f19" + integrity sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ== dependencies: - buffer-equal "1.0.0" - colors "1.0.3" - commander "2.9.0" - minimatch "3.0.4" + minimatch "^3.0.5" + p-limit "^3.1.0 " dom-serializer@^2.0.0: version "2.0.0" @@ -1307,34 +1291,35 @@ es6-error@^4.1.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== +esbuild@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" escape-string-regexp@^1.0.5: version "1.0.5" @@ -1397,10 +1382,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1439,6 +1424,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.1.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -1448,16 +1442,6 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1468,6 +1452,11 @@ fsevents@~2.3.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1494,7 +1483,7 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -glob-parent@^5.1.1, glob-parent@~5.1.0: +glob-parent@^5.1.1, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1571,11 +1560,6 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= - gulp-merge-json@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/gulp-merge-json/-/gulp-merge-json-2.1.1.tgz#cfb1d066467577545b8c1c289a278e6ef4b4e0de" @@ -1587,28 +1571,18 @@ gulp-merge-json@^2.1.1: through "^2.3.8" vinyl "^2.1.0" -gulp-shell@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/gulp-shell/-/gulp-shell-0.8.0.tgz#0ed4980de1d0c67e5f6cce971d7201fd0be50555" - integrity sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ== +gulp-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/gulp-sort/-/gulp-sort-2.0.0.tgz#c6762a2f1f0de0a3fc595a21599d3fac8dba1aca" + integrity sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g== dependencies: - chalk "^3.0.0" - fancy-log "^1.3.3" - lodash.template "^4.5.0" - plugin-error "^1.0.1" - through2 "^3.0.1" - tslib "^1.10.0" + through2 "^2.0.1" has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -1887,31 +1861,11 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - lodash.mergewith@^4.6.1: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.template@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -1984,19 +1938,26 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@^3.0.3, minimatch@^3.0.5, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.3, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.3: version "1.2.6" @@ -2033,15 +1994,10 @@ mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.14.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== - -nan@^2.17.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== +nan@^2.18.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" + integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw== napi-build-utils@^1.0.1: version "1.0.2" @@ -2104,7 +2060,7 @@ object-keys@^1.0.12: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -2125,6 +2081,13 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +"p-limit@^3.1.0 ": + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + parse-node-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -2190,6 +2153,15 @@ plist@^3.0.1: base64-js "^1.5.1" xmlbuilder "^9.0.7" +plist@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" + integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + plugin-error@1.0.1, plugin-error@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" @@ -2200,7 +2172,7 @@ plugin-error@1.0.1, plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -prebuild-install@^7.0.1, prebuild-install@^7.1.1: +prebuild-install@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== @@ -2218,6 +2190,24 @@ prebuild-install@^7.0.1, prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +prebuild-install@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + priorityqueuejs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz#2ee4f23c2560913e08c07ce5ccdd6de3df2c5af8" @@ -2297,6 +2287,19 @@ readable-stream@^2.0.2, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -2304,6 +2307,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -2444,6 +2454,11 @@ stoppable@^1.1.0: resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== +stream-exhaust@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" + integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw== + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" @@ -2504,13 +2519,6 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -2542,6 +2550,14 @@ ternary-stream@^3.0.0: merge-stream "^2.0.0" through2 "^3.0.1" +through2@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + through2@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" @@ -2586,35 +2602,26 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -tree-sitter-typescript@^0.20.3: - version "0.20.3" - resolved "https://registry.yarnpkg.com/tree-sitter-typescript/-/tree-sitter-typescript-0.20.3.tgz#454560314c419f5364cd4585a48d675e44f55edc" - integrity sha512-5+RZ9G3/VOxxSzyniVc5dfNhfan1eOxQvUdTgXhpsGIYlmSW3HwIuPEJ7r65FWH2WnJWirOu11Pm0usmkx2JOg== - dependencies: - nan "^2.14.0" - -tree-sitter@^0.20.5: +tree-sitter-typescript@^0.20.5: version "0.20.5" - resolved "https://registry.yarnpkg.com/tree-sitter/-/tree-sitter-0.20.5.tgz#554741ee06b984824dd5082353aa2a28bcefa271" - integrity sha512-xjxkKCKV7F2F5HWmyRE4bosoxkbxe9lYvFRc/nzmtHNqFNTwYwh0oWVVEt0VnbupZHMirEQW7vDx8ddJn72tjg== + resolved "https://registry.yarnpkg.com/tree-sitter-typescript/-/tree-sitter-typescript-0.20.5.tgz#29e30c052bcb06cb992ffd2d392e010b0e1768b3" + integrity sha512-RzK/Pc6k4GiXvInIBlo8ZggekP6rODfW2P6KHFCTSUHENsw6ynh+iacFhfkJRa4MT8EIN2WHygFJ7076/+eHKg== dependencies: - nan "^2.17.0" + nan "^2.18.0" + tree-sitter "^0.20.6" + +tree-sitter@^0.20.5, tree-sitter@^0.20.6: + version "0.20.6" + resolved "https://registry.yarnpkg.com/tree-sitter/-/tree-sitter-0.20.6.tgz#fec52e5d7cc6c583135756479f2440dd89b25cbe" + integrity sha512-GxJodajVpfgb3UREzzIbtA1hyRnTxVbWVXrbC6sk4xTMH5ERMBJk9HJNq4c8jOJeUaIOmLcwg+t6mez/PDvGqg== + dependencies: + nan "^2.18.0" prebuild-install "^7.1.1" -tslib@^1.10.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== - -tslib@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.0.0, tslib@^2.2.0, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tunnel-agent@^0.6.0: version "0.6.0" @@ -2727,16 +2734,18 @@ vscode-gulp-watch@^5.0.3: vinyl "^2.2.0" vinyl-file "^3.0.0" -vscode-universal-bundler@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/vscode-universal-bundler/-/vscode-universal-bundler-0.0.2.tgz#2c988dac681d3ffe6baec6defac0995cb833c55a" - integrity sha512-FPJcvKnQGBqFzy6M6Nm2yvAczNLUeXsfYM6GwCex/pUOkvIM2icIHmiSvtMJINlLW1iG+oEwE3/LVbABmcjEmQ== +vscode-universal-bundler@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/vscode-universal-bundler/-/vscode-universal-bundler-0.1.0.tgz#1a03d1d16c6ea5065318fafbc2a554b7c2f3bd32" + integrity sha512-wtT9IZ/fqIZSirY6cxElu8a6WpNaOjgQjjazt85lMCWBuF/tWVw5nRYX67pTVsUyi6kzQaIvfyyIvxVbBpetBA== dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - asar "^3.0.3" + "@electron/asar" "^3.2.7" + "@malept/cross-spawn-promise" "^2.0.0" debug "^4.3.1" - dir-compare "^2.4.0" - fs-extra "^9.0.1" + dir-compare "^4.2.0" + fs-extra "^11.1.1" + minimatch "^9.0.3" + plist "^3.1.0" webidl-conversions@^3.0.0: version "3.0.1" @@ -2776,6 +2785,11 @@ xml2js@^0.4.19, xml2js@^0.4.23: sax ">=0.6.0" xmlbuilder "~11.0.0" +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" @@ -2786,6 +2800,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -2805,3 +2824,8 @@ yazl@^2.2.2: integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== dependencies: buffer-crc32 "~0.2.3" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/cglicenses.json b/cglicenses.json index 6ca8ba33b4d..268a7e1dd1b 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -130,6 +130,21 @@ "SOFTWARE" ] }, + { + // Reason: NPM package does not include repository URL https://github.com/microsoft/vscode-deviceid/issues/12 + "name": "@vscode/deviceid", + "fullLicenseText": [ + "Copyright (c) Microsoft Corporation.", + "", + "MIT License", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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." + ] + }, { // Reason: Missing license file "name": "@tokenizer/token", @@ -317,13 +332,21 @@ "fullLicenseTextUri": "https://raw.githubusercontent.com/rodrimati1992/const_format_crates/b2207af46bfbd9f1a6bd12dbffd10feeea3d9fd7/LICENSE-ZLIB.md" }, { // License is MIT/Apache and tool doesn't look in subfolders - "name": "toml", - "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml/LICENSE-MIT" + "name": "toml_edit", + "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml_edit/LICENSE-MIT" + }, + { // License is MIT/Apache and tool doesn't look in subfolders + "name": "toml_datetime", + "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml_datetime/LICENSE-MIT" }, { // License is MIT/Apache and tool doesn't look in subfolders "name": "dirs-sys-next", "fullLicenseTextUri": "https://raw.githubusercontent.com/xdg-rs/dirs/master/dirs-sys/LICENSE-MIT" }, + { // License is MIT/Apache and gitlab API doesn't find the project + "name": "libredox", + "fullLicenseTextUri": "https://gitlab.redox-os.org/redox-os/libredox/-/raw/master/LICENSE" + }, { "name": "https-proxy-agent", "fullLicenseText": [ @@ -545,49 +568,29 @@ ] }, { - // Reason: missing repository property on package.json - // Issue: https://github.com/microsoft/vscode-v8-heap-tools/issues/14 - "name": "@vscode/v8-heap-parser", + "name":"vscode-markdown-languageserver", "fullLicenseText": [ - "The code in this package is built upon that in the Chrome devtools", - "(https://github.com/ChromeDevTools/devtools-frontend) which is made available", - "under the following license:", + "MIT License", "", - "// Copyright 2014 The Chromium Authors. All rights reserved.", - "//", - "// Redistribution and use in source and binary forms, with or without", - "// modification, are permitted provided that the following conditions are", - "// met:", - "//", - "// * Redistributions of source code must retain the above copyright", - "// notice, this list of conditions and the following disclaimer.", - "// * Redistributions in binary form must reproduce the above", - "// copyright notice, this list of conditions and the following disclaimer", - "// in the documentation and/or other materials provided with the", - "// distribution.", - "// * Neither the name of Google Inc. nor the names of its", - "// contributors may be used to endorse or promote products derived from", - "// this software without specific prior written permission.", - "//", - "// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS", - "// 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT", - "// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR", - "// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT", - "// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,", - "// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT", - "// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,", - "// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY", - "// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT", - "// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE", - "// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." - ] - }, - { - // Reason: missing copyright statement - // Issue: https://github.com/qiao/heap.js/issues/33 - "name": "heap", - "prependLicenseText": [ - "Copyright (c) heap.js authors" + "Copyright (c) Microsoft Corporation.", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "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" ] }, { @@ -599,5 +602,23 @@ // Reason: mono-repo where the individual packages are also dual-licensed under MIT and Apache-2.0 "name": "system-configuration-sys", "fullLicenseTextUri": "https://raw.githubusercontent.com/mullvad/system-configuration-rs/v0.6.0/system-configuration-sys/LICENSE-MIT" + }, + { + // Reason: License missing from the repository https://github.com/isaacs/chownr/issues/35 + "name": "chownr", + "fullLicenseText": [ + "The ISC License", + "Copyright (c) Isaac Z. Schlueter and Contributors", + "Permission to use, copy, modify, and/or distribute this software for any", + "purpose with or without fee is hereby granted, provided that the above", + "copyright notice and this permission notice appear in all copies.", + "THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES", + "WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF", + "MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR", + "ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES", + "WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN", + "ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR", + "IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE." + ] } ] diff --git a/cgmanifest.json b/cgmanifest.json index 891a0b0cb32..aa06f8323b0 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "14d11e5bb9b5b1cd51f7b19546e74a73cab42084" + "commitHash": "7fa4f6e14e0707c0e604cf7c1da33566e78169ce" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "120.0.6099.291" + "version": "124.0.6367.243" }, { "component": { @@ -48,7 +48,7 @@ "git": { "name": "ffmpeg", "repositoryUrl": "https://chromium.googlesource.com/chromium/third_party/ffmpeg", - "commitHash": "e1ca3f06adec15150a171bc38f550058b4bbb23b" + "commitHash": "52d8ef3799b2f16b66351dd0972bb0bcee1648ac" } }, "isOnlyProductionDependency": true, @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "8a01b3dcb7d08a48bfd3e6bf85ef49faa1454839" + "commitHash": "fe0f08a5dd68fd72b1652adaa51ab07a4b09f847" } }, "isOnlyProductionDependency": true, - "version": "18.18.2" + "version": "20.14.0" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "31cd9d1f61714e20f1067d726404600ab7281698" + "commitHash": "91de7d0f13208891c5604e00ccd18e4f6826653b" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "28.2.8" + "version": "30.1.2" }, { "component": { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 4be3e46eb7f..27fe79896a2 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -10,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -34,51 +43,51 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -87,113 +96,176 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" -version = "1.8.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ - "concurrent-queue 2.2.0", - "event-listener", + "concurrent-queue", + "event-listener-strategy 0.5.2", "futures-core", + "pin-project-lite", ] [[package]] name = "async-io" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ + "async-lock 2.8.0", "autocfg", - "concurrent-queue 1.2.4", - "futures-lite", - "libc", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", "log", - "once_cell", "parking", - "polling", + "polling 2.8.0", + "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", - "winapi", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.0", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "event-listener", + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", ] [[package]] name = "async-process" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-io", - "async-lock", - "autocfg", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", "blocking", "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "signal-hook", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.34", "windows-sys 0.48.0", ] [[package]] name = "async-recursion" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", +] + +[[package]] +name = "async-signal" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +dependencies = [ + "async-io 2.3.2", + "async-lock 3.3.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.34", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", ] [[package]] name = "async-task" -version = "4.4.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] [[package]] name = "base64" -version = "0.21.2" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bit-vec" @@ -209,72 +281,65 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.3.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", - "async-lock", + "async-lock 3.3.0", "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "log", + "futures-io", + "futures-lite 2.3.0", + "piper", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -284,58 +349,56 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "winapi", + "windows-targets 0.52.5", ] [[package]] name = "clap" -version = "4.3.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.0" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", - "bitflags 1.3.2", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "code-cli" @@ -389,67 +452,48 @@ dependencies = [ "zip", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "1.2.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" -dependencies = [ - "cache-padded", -] - -[[package]] -name = "concurrent-queue" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] name = "const_format" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ "proc-macro2", "quote", @@ -458,9 +502,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -468,46 +512,42 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "58ebf8d6963185c7625d2c3c3962d99eb8936637b1427536d21dc36ae402ebad" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -519,55 +559,20 @@ dependencies = [ "typenum", ] -[[package]] -name = "cxx" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88abab2f5abbe4c56e8f1fb431b784d710b709888f35755a160e62e33fe38e8" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0c11acd0e63bae27dcd2afced407063312771212b7a823b4fd72d633be30fb" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.18", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3816ed957c008ccd4728485511e3d9aaf7db419aa321e3d2c5a2f3411e36c8" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26acccf6f445af85ea056362561a24ef56cdc15fcc685f03aec50b9c702cb6d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] [[package]] name = "derivative" @@ -577,7 +582,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] @@ -594,9 +599,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -652,18 +657,18 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] name = "enumflags2" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" dependencies = [ "enumflags2_derive", "serde", @@ -671,13 +676,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] @@ -688,23 +693,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -714,31 +708,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] -name = "fastrand" -version = "1.8.0" +name = "event-listener" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] -name = "filetime" -version = "0.2.17" +name = "fastrand" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.36.1", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "libz-sys", @@ -768,18 +821,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -792,9 +845,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -802,15 +855,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -819,17 +872,17 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -839,33 +892,43 @@ dependencies = [ ] [[package]] -name = "futures-macro" -version = "0.3.28" +name = "futures-lite" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -881,9 +944,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -896,7 +959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -912,9 +975,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -922,10 +985,16 @@ dependencies = [ ] [[package]] -name = "h2" -version = "0.3.24" +name = "gimli" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -933,7 +1002,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -948,30 +1017,21 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -996,9 +1056,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1007,9 +1067,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1024,15 +1084,15 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1045,7 +1105,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -1067,33 +1127,32 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1101,9 +1160,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -1111,19 +1170,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] name = "indicatif" -version = "0.17.4" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db45317f37ef454e6519b6c3ed7d377e5f23346f0823f86e65ca36912d1d0ef8" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", @@ -1144,9 +1203,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -1157,16 +1216,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" -version = "2.5.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-docker" @@ -1177,18 +1236,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -1200,32 +1247,38 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.4" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "keyring" -version = "2.0.3" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e319fe0cb5b29a55cdb228df3f651b6c8cdc5b19520f3e62c8f111dc2582026c" +checksum = "363387f0019d714aa60cc30ab4fe501a747f4c08fc58f069dd14be971bd495a0" dependencies = [ "byteorder", "lazy_static", "linux-keyutils", "secret-service", "security-framework", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1236,37 +1289,38 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] [[package]] name = "libz-sys" -version = "1.1.12" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" -dependencies = [ - "cc", -] - [[package]] name = "linux-keyutils" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f27bb67f6dd1d0bb5ab582868e4f65052e58da6401188a08f0da09cf512b84b" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "libc", ] @@ -1277,10 +1331,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] -name = "lock_api" -version = "0.4.9" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1288,9 +1348,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.18" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "md5" @@ -1300,9 +1360,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" @@ -1314,16 +1374,25 @@ dependencies = [ ] [[package]] -name = "mime" -version = "0.3.16" +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -1359,31 +1428,30 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", - "static_assertions", + "memoffset 0.7.1", ] [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] [[package]] name = "num" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -1395,11 +1463,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", "rand 0.8.5", @@ -1407,28 +1474,33 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -1437,11 +1509,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -1449,20 +1520,20 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", ] @@ -1473,28 +1544,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "once_cell" -version = "1.17.2" +name = "object" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16814a067484415fda653868c9be0ac5f2abd2ef5d951082a5f2fe1b3662944" +checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" dependencies = [ "is-wsl", + "libc", "pathdiff", ] [[package]] name = "openssl" -version = "0.10.60" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1505,13 +1586,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] @@ -1522,9 +1603,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.96" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -1550,7 +1631,7 @@ checksum = "ed41783a5bf567688eb38372f2b7a8530f5a607a4b49d38dd7573236c23ca7e2" dependencies = [ "futures-channel", "futures-util", - "indexmap 1.9.1", + "indexmap 1.9.3", "once_cell", "pin-project-lite", "thiserror", @@ -1595,25 +1676,25 @@ dependencies = [ [[package]] name = "os_info" -version = "3.7.0" +version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" dependencies = [ "log", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "parking" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1621,22 +1702,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.5.1", "smallvec", - "windows-sys 0.36.1", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.9" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -1646,35 +1727,35 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1683,62 +1764,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.25" +name = "piper" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" -version = "2.3.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", + "bitflags 1.3.2", "cfg-if", + "concurrent-queue", "libc", "log", - "wepoll-ffi", - "winapi", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "portable-atomic" -version = "1.3.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "thiserror", - "toml", + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1802,7 +1916,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.15", ] [[package]] @@ -1816,38 +1930,50 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.7", - "redox_syscall 0.2.16", + "getrandom 0.2.15", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.8.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1856,15 +1982,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64", "bytes", @@ -1884,9 +2010,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -1902,9 +2030,9 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.11" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", @@ -1913,9 +2041,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ "byteorder", "rmp", @@ -1925,7 +2053,7 @@ dependencies = [ [[package]] name = "russh" version = "0.37.1" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "async-trait", "bitflags 1.3.2", @@ -1954,7 +2082,7 @@ dependencies = [ [[package]] name = "russh-cryptovec" version = "0.7.0" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "libc", "winapi", @@ -1963,7 +2091,7 @@ dependencies = [ [[package]] name = "russh-keys" version = "0.37.1" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "bit-vec", "byteorder", @@ -1988,46 +2116,67 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.37.25" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] -name = "ryu" -version = "1.0.11" +name = "rustix" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.52.0", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secret-service" @@ -2047,11 +2196,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -2060,9 +2209,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2070,38 +2219,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.9" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2110,13 +2259,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] @@ -2133,9 +2282,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2144,9 +2293,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2165,50 +2314,50 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" -[[package]] -name = "signal-hook" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2217,21 +2366,21 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2240,20 +2389,26 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sysinfo" -version = "0.29.0" +version = "0.29.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2286,9 +2441,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" dependencies = [ "filetime", "libc", @@ -2297,61 +2452,54 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", + "num-conv", + "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" @@ -2364,17 +2512,17 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -2382,7 +2530,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -2390,13 +2538,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] @@ -2411,9 +2559,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2436,9 +2584,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -2446,16 +2594,23 @@ dependencies = [ "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] -name = "toml" -version = "0.5.9" +name = "toml_datetime" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "serde", + "indexmap 2.2.6", + "toml_datetime", + "winnow", ] [[package]] @@ -2466,11 +2621,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2478,29 +2632,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" @@ -2552,46 +2706,47 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ + "memoffset 0.9.1", "tempfile", "winapi", ] [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -2601,9 +2756,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "url" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -2630,11 +2785,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.15", "serde", ] @@ -2652,17 +2807,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -2680,9 +2834,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2690,24 +2844,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2717,9 +2871,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2727,28 +2881,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -2759,23 +2913,14 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2792,15 +2937,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2808,34 +2944,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -2844,158 +2958,144 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_gnu" -version = "0.48.0" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.4.1" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -3021,20 +3121,22 @@ dependencies = [ [[package]] name = "xattr" -version = "0.2.3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", ] [[package]] name = "xdg-home" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" dependencies = [ - "nix", + "libc", "winapi", ] @@ -3050,9 +3152,9 @@ dependencies = [ [[package]] name = "zbus" -version = "3.13.1" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3d77c9966c28321f1907f0b6c5a5561189d1f7311eea6d94180c6be9daab29" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", "async-process", @@ -3061,7 +3163,7 @@ dependencies = [ "byteorder", "derivative", "enumflags2", - "event-listener", + "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", @@ -3086,24 +3188,23 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.13.1" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e341d12edaff644e539ccbbf7f161601294c9a84ed3d7e015da33155b435af" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", - "syn 1.0.103", - "winnow", + "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.5.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", @@ -3112,9 +3213,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zip" @@ -3131,9 +3232,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.14.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" dependencies = [ "byteorder", "enumflags2", @@ -3145,14 +3246,14 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.14.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", "zvariant_utils", ] @@ -3164,5 +3265,5 @@ checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index db058cd9f7c..b820ffcc50f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -35,12 +35,12 @@ chrono = { version = "0.4.26", features = ["serde", "std", "clock"], default-fea gethostname = "0.4.3" libc = "0.2.144" tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30", default-features = false, features = ["connections"] } -keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl"] } +keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl", "platform-windows", "platform-macos", "linux-keyutils"] } dialoguer = "0.10.4" hyper = { version = "0.14.26", features = ["server", "http1", "runtime"] } indicatif = "0.17.4" tempfile = "3.5.0" -clap_lex = "0.5.0" +clap_lex = "0.7.0" url = "2.3.1" async-trait = "0.1.68" log = "0.4.18" diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index f65f8a33397..8d04738ba05 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -17,6 +17,38 @@ required to debug changes to any libraries licensed under the GNU Lesser General +--------------------------------------------------------- + +addr2line 0.21.0 - Apache-2.0 OR MIT +https://github.com/gimli-rs/addr2line + +Copyright (c) 2016-2018 The gimli Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + --------------------------------------------------------- adler 1.0.2 - 0BSD OR MIT OR Apache-2.0 @@ -49,7 +81,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -aho-corasick 1.0.1 - Unlicense OR MIT +aho-corasick 1.1.3 - Unlicense OR MIT https://github.com/BurntSushi/aho-corasick The MIT License (MIT) @@ -132,7 +164,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstream 0.3.2 - MIT OR Apache-2.0 +anstream 0.6.14 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -157,7 +189,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle 1.0.0 - MIT OR Apache-2.0 +anstyle 1.0.7 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -182,7 +214,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle-parse 0.2.0 - MIT OR Apache-2.0 +anstyle-parse 0.2.4 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -207,7 +239,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle-query 1.0.0 - MIT OR Apache-2.0 +anstyle-query 1.0.3 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -232,7 +264,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle-wincon 1.0.1 - MIT OR Apache-2.0 +anstyle-wincon 3.0.3 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -285,7 +317,7 @@ SOFTWARE. --------------------------------------------------------- -async-channel 1.8.0 - Apache-2.0 OR MIT +async-channel 2.3.1 - Apache-2.0 OR MIT https://github.com/smol-rs/async-channel Permission is hereby granted, free of charge, to any @@ -315,7 +347,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-io 1.9.0 - Apache-2.0 OR MIT +async-io 1.13.0 - Apache-2.0 OR MIT +async-io 2.3.2 - Apache-2.0 OR MIT https://github.com/smol-rs/async-io Permission is hereby granted, free of charge, to any @@ -345,7 +378,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-lock 2.7.0 - Apache-2.0 OR MIT +async-lock 2.8.0 - Apache-2.0 OR MIT +async-lock 3.3.0 - Apache-2.0 OR MIT https://github.com/smol-rs/async-lock Permission is hereby granted, free of charge, to any @@ -375,7 +409,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-process 1.7.0 - Apache-2.0 OR MIT +async-process 1.8.1 - Apache-2.0 OR MIT https://github.com/smol-rs/async-process Permission is hereby granted, free of charge, to any @@ -405,7 +439,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-recursion 1.0.0 - MIT OR Apache-2.0 +async-recursion 1.1.1 - MIT OR Apache-2.0 https://github.com/dcchut/async-recursion Permission is hereby granted, free of charge, to any @@ -435,7 +469,37 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-task 4.4.0 - Apache-2.0 OR MIT +async-signal 0.2.6 - Apache-2.0 OR MIT +https://github.com/smol-rs/async-signal + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +async-task 4.7.1 - Apache-2.0 OR MIT https://github.com/smol-rs/async-task Permission is hereby granted, free of charge, to any @@ -465,7 +529,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-trait 0.1.68 - MIT OR Apache-2.0 +async-trait 0.1.80 - MIT OR Apache-2.0 https://github.com/dtolnay/async-trait Permission is hereby granted, free of charge, to any @@ -495,7 +559,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -atomic-waker 1.1.1 - Apache-2.0 OR MIT +atomic-waker 1.1.2 - Apache-2.0 OR MIT https://github.com/smol-rs/atomic-waker Permission is hereby granted, free of charge, to any @@ -525,7 +589,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -autocfg 1.1.0 - Apache-2.0 OR MIT +autocfg 1.3.0 - Apache-2.0 OR MIT https://github.com/cuviper/autocfg Copyright (c) 2018 Josh Stone @@ -557,7 +621,39 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -base64 0.21.2 - MIT OR Apache-2.0 +backtrace 0.3.71 - MIT OR Apache-2.0 +https://github.com/rust-lang/backtrace-rs + +Copyright (c) 2014 Alex Crichton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +base64 0.21.7 - MIT OR Apache-2.0 https://github.com/marshallpierce/rust-base64 The MIT License (MIT) @@ -618,7 +714,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- bitflags 1.3.2 - MIT/Apache-2.0 -bitflags 2.4.1 - MIT OR Apache-2.0 +bitflags 2.5.0 - MIT OR Apache-2.0 https://github.com/bitflags/bitflags Copyright (c) 2014 The Rust Project Developers @@ -650,7 +746,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -block-buffer 0.10.3 - MIT OR Apache-2.0 +block-buffer 0.10.4 - MIT OR Apache-2.0 https://github.com/RustCrypto/utils All crates licensed under either of @@ -704,7 +800,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -block-padding 0.3.2 - MIT OR Apache-2.0 +block-padding 0.3.3 - MIT OR Apache-2.0 https://github.com/RustCrypto/utils All crates licensed under either of @@ -758,7 +854,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -blocking 1.3.1 - Apache-2.0 OR MIT +blocking 1.6.0 - Apache-2.0 OR MIT https://github.com/smol-rs/blocking Permission is hereby granted, free of charge, to any @@ -788,7 +884,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -bumpalo 3.12.0 - MIT/Apache-2.0 +bumpalo 3.16.0 - MIT OR Apache-2.0 https://github.com/fitzgen/bumpalo Copyright (c) 2019 Nick Fitzgerald @@ -820,7 +916,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -byteorder 1.4.3 - Unlicense OR MIT +byteorder 1.5.0 - Unlicense OR MIT https://github.com/BurntSushi/byteorder The MIT License (MIT) @@ -848,7 +944,7 @@ THE SOFTWARE. --------------------------------------------------------- -bytes 1.4.0 - MIT +bytes 1.6.0 - MIT https://github.com/tokio-rs/bytes The MIT License (MIT) @@ -882,37 +978,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -cache-padded 1.2.0 - Apache-2.0 OR MIT -https://github.com/smol-rs/cache-padded - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - -cc 1.0.73 - MIT/Apache-2.0 +cc 1.0.98 - MIT OR Apache-2.0 https://github.com/rust-lang/cc-rs Copyright (c) 2014 Alex Crichton @@ -976,7 +1042,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -chrono 0.4.26 - MIT/Apache-2.0 +chrono 0.4.38 - MIT OR Apache-2.0 https://github.com/chronotope/chrono Rust-chrono is dual-licensed under The MIT License [1] and @@ -1222,7 +1288,7 @@ limitations under the License. --------------------------------------------------------- -clap 4.3.0 - MIT OR Apache-2.0 +clap 4.5.4 - MIT OR Apache-2.0 https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1248,7 +1314,7 @@ SOFTWARE. --------------------------------------------------------- -clap_builder 4.3.0 - MIT OR Apache-2.0 +clap_builder 4.5.2 - MIT OR Apache-2.0 https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1274,8 +1340,8 @@ SOFTWARE. --------------------------------------------------------- -clap_derive 4.3.0 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_derive +clap_derive 4.5.4 - MIT OR Apache-2.0 +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1300,8 +1366,8 @@ SOFTWARE. --------------------------------------------------------- -clap_lex 0.5.0 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_lex +clap_lex 0.7.0 - MIT OR Apache-2.0 +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1326,215 +1392,7 @@ SOFTWARE. --------------------------------------------------------- -codespan-reporting 0.11.1 - Apache-2.0 -https://github.com/brendanzab/codespan - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---------------------------------------------------------- - ---------------------------------------------------------- - -colorchoice 1.0.0 - MIT OR Apache-2.0 +colorchoice 1.0.1 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -1559,8 +1417,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -concurrent-queue 1.2.4 - Apache-2.0 OR MIT -concurrent-queue 2.2.0 - Apache-2.0 OR MIT +concurrent-queue 2.5.0 - Apache-2.0 OR MIT https://github.com/smol-rs/concurrent-queue Permission is hereby granted, free of charge, to any @@ -1590,7 +1447,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -console 0.15.7 - MIT +console 0.15.8 - MIT https://github.com/console-rs/console The MIT License (MIT) @@ -1618,7 +1475,7 @@ SOFTWARE. --------------------------------------------------------- -const_format 0.2.31 - Zlib +const_format 0.2.32 - Zlib https://github.com/rodrimati1992/const_format_crates/ Copyright (c) 2020 Matias Rodriguez. @@ -1642,7 +1499,7 @@ freely, subject to the following restrictions: --------------------------------------------------------- -const_format_proc_macros 0.2.31 - Zlib +const_format_proc_macros 0.2.32 - Zlib https://github.com/rodrimati1992/const_format_crates/ Copyright (c) 2020 Matias Rodriguez. @@ -1666,7 +1523,7 @@ freely, subject to the following restrictions: --------------------------------------------------------- -core-foundation 0.9.3 - MIT / Apache-2.0 +core-foundation 0.9.4 - MIT OR Apache-2.0 https://github.com/servo/core-foundation-rs Copyright (c) 2012-2013 Mozilla Foundation @@ -1698,7 +1555,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -core-foundation-sys 0.8.3 - MIT / Apache-2.0 +core-foundation-sys 0.8.6 - MIT OR Apache-2.0 https://github.com/servo/core-foundation-rs Copyright (c) 2012-2013 Mozilla Foundation @@ -1730,7 +1587,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -cpufeatures 0.2.8 - MIT OR Apache-2.0 +cpufeatures 0.2.12 - MIT OR Apache-2.0 https://github.com/RustCrypto/utils All crates licensed under either of @@ -1784,7 +1641,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -crc32fast 1.3.2 - MIT OR Apache-2.0 +crc32fast 1.4.1 - MIT OR Apache-2.0 https://github.com/srijs/rust-crc32fast MIT License @@ -1812,7 +1669,7 @@ SOFTWARE. --------------------------------------------------------- -crossbeam-channel 0.5.6 - MIT OR Apache-2.0 +crossbeam-channel 0.5.13 - MIT OR Apache-2.0 https://github.com/crossbeam-rs/crossbeam The MIT License (MIT) @@ -1846,7 +1703,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -crossbeam-utils 0.8.16 - MIT OR Apache-2.0 +crossbeam-utils 0.8.20 - MIT OR Apache-2.0 https://github.com/crossbeam-rs/crossbeam The MIT License (MIT) @@ -1936,127 +1793,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -cxx 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - -cxx-build 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - -cxxbridge-flags 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - -cxxbridge-macro 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - -data-encoding 2.3.2 - MIT +data-encoding 2.6.0 - MIT https://github.com/ia0/data-encoding The MIT License (MIT) @@ -2085,6 +1822,32 @@ SOFTWARE. --------------------------------------------------------- +deranged 0.3.11 - MIT OR Apache-2.0 +https://github.com/jhpratt/deranged + +Copyright (c) 2022 Jacob Pratt et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. +--------------------------------------------------------- + +--------------------------------------------------------- + derivative 2.2.0 - MIT/Apache-2.0 https://github.com/mcarton/rust-derivative @@ -2139,7 +1902,7 @@ SOFTWARE. --------------------------------------------------------- -digest 0.10.5 - MIT OR Apache-2.0 +digest 0.10.7 - MIT OR Apache-2.0 https://github.com/RustCrypto/traits All crates licensed under either of @@ -2273,7 +2036,7 @@ SOFTWARE --------------------------------------------------------- -encoding_rs 0.8.31 - (Apache-2.0 OR MIT) AND BSD-3-Clause +encoding_rs 0.8.34 - (Apache-2.0 OR MIT) AND BSD-3-Clause https://github.com/hsivonen/encoding_rs Copyright Mozilla Foundation @@ -2305,7 +2068,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -enumflags2 0.7.7 - MIT OR Apache-2.0 +enumflags2 0.7.9 - MIT OR Apache-2.0 https://github.com/meithecatte/enumflags2 Copyright (c) 2017-2023 Maik Klein, Maja Kądziołka @@ -2337,7 +2100,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -enumflags2_derive 0.7.7 - MIT OR Apache-2.0 +enumflags2_derive 0.7.9 - MIT OR Apache-2.0 https://github.com/meithecatte/enumflags2 Copyright (c) 2017-2023 Maik Klein, Maja Kądziołka @@ -2401,7 +2164,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -errno 0.3.1 - MIT OR Apache-2.0 +errno 0.3.9 - MIT OR Apache-2.0 https://github.com/lambda-fairy/rust-errno Copyright (c) 2014 Chris Wong @@ -2433,35 +2196,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -errno-dragonfly 0.1.2 - MIT -https://github.com/mneumann/errno-dragonfly-rs - -MIT License - -Copyright (c) 2017 Michael Neumann - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -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. ---------------------------------------------------------- - ---------------------------------------------------------- - event-listener 2.5.3 - Apache-2.0 OR MIT +event-listener 3.1.0 - Apache-2.0 OR MIT +event-listener 4.0.3 - Apache-2.0 OR MIT +event-listener 5.3.0 - Apache-2.0 OR MIT https://github.com/smol-rs/event-listener Permission is hereby granted, free of charge, to any @@ -2491,7 +2229,39 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -fastrand 1.8.0 - Apache-2.0 OR MIT +event-listener-strategy 0.4.0 - Apache-2.0 OR MIT +event-listener-strategy 0.5.2 - Apache-2.0 OR MIT +https://github.com/smol-rs/event-listener-strategy + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +fastrand 1.9.0 - Apache-2.0 OR MIT +fastrand 2.1.0 - Apache-2.0 OR MIT https://github.com/smol-rs/fastrand Permission is hereby granted, free of charge, to any @@ -2521,7 +2291,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -filetime 0.2.17 - MIT/Apache-2.0 +filetime 0.2.23 - MIT/Apache-2.0 https://github.com/alexcrichton/filetime Copyright (c) 2014 Alex Crichton @@ -2553,7 +2323,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -flate2 1.0.26 - MIT OR Apache-2.0 +flate2 1.0.30 - MIT OR Apache-2.0 https://github.com/rust-lang/flate2-rs Copyright (c) 2014 Alex Crichton @@ -2669,7 +2439,7 @@ SOFTWARE. --------------------------------------------------------- -form_urlencoded 1.1.0 - MIT OR Apache-2.0 +form_urlencoded 1.2.1 - MIT OR Apache-2.0 https://github.com/servo/rust-url Copyright (c) 2013-2022 The rust-url developers @@ -2701,7 +2471,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures 0.3.28 - MIT OR Apache-2.0 +futures 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2734,7 +2504,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-channel 0.3.28 - MIT OR Apache-2.0 +futures-channel 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2767,7 +2537,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-core 0.3.28 - MIT OR Apache-2.0 +futures-core 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2800,7 +2570,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-executor 0.3.28 - MIT OR Apache-2.0 +futures-executor 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2833,7 +2603,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-io 0.3.28 - MIT OR Apache-2.0 +futures-io 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2866,7 +2636,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-lite 1.12.0 - Apache-2.0 OR MIT +futures-lite 1.13.0 - Apache-2.0 OR MIT +futures-lite 2.3.0 - Apache-2.0 OR MIT https://github.com/smol-rs/futures-lite Permission is hereby granted, free of charge, to any @@ -2896,7 +2667,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-macro 0.3.28 - MIT OR Apache-2.0 +futures-macro 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2929,7 +2700,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-sink 0.3.28 - MIT OR Apache-2.0 +futures-sink 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2962,7 +2733,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-task 0.3.28 - MIT OR Apache-2.0 +futures-task 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2995,7 +2766,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-util 0.3.28 - MIT OR Apache-2.0 +futures-util 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -3028,7 +2799,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -generic-array 0.14.6 - MIT +generic-array 0.14.7 - MIT https://github.com/fizyk20/generic-array The MIT License (MIT) @@ -3265,7 +3036,7 @@ limitations under the License. --------------------------------------------------------- getrandom 0.1.16 - MIT OR Apache-2.0 -getrandom 0.2.7 - MIT OR Apache-2.0 +getrandom 0.2.15 - MIT OR Apache-2.0 https://github.com/rust-random/getrandom Copyright (c) 2018-2024 The rust-random Project Developers @@ -3298,7 +3069,39 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -h2 0.3.24 - MIT +gimli 0.28.1 - MIT OR Apache-2.0 +https://github.com/gimli-rs/gimli + +Copyright (c) 2015 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +h2 0.3.26 - MIT https://github.com/hyperium/h2 The MIT License (MIT) @@ -3333,7 +3136,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- hashbrown 0.12.3 - MIT OR Apache-2.0 -hashbrown 0.14.3 - MIT OR Apache-2.0 +hashbrown 0.14.5 - MIT OR Apache-2.0 https://github.com/rust-lang/hashbrown Copyright (c) 2016 Amanieu d'Antras @@ -3365,7 +3168,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -heck 0.4.0 - MIT OR Apache-2.0 +heck 0.5.0 - MIT OR Apache-2.0 https://github.com/withoutboats/heck Copyright (c) 2015 The Rust Project Developers @@ -3397,8 +3200,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -hermit-abi 0.1.19 - MIT/Apache-2.0 -hermit-abi 0.3.1 - MIT OR Apache-2.0 +hermit-abi 0.3.9 - MIT OR Apache-2.0 https://github.com/hermit-os/hermit-rs Permission is hereby granted, free of charge, to any @@ -3557,7 +3359,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -http 0.2.9 - MIT OR Apache-2.0 +http 0.2.12 - MIT OR Apache-2.0 https://github.com/hyperium/http Copyright (c) 2017 http-rs authors @@ -3589,12 +3391,12 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -http-body 0.4.5 - MIT +http-body 0.4.6 - MIT https://github.com/hyperium/http-body The MIT License (MIT) -Copyright (c) 2019 Hyper Contributors +Copyright (c) 2019-2024 Sean McArthur & Hyper Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -3626,7 +3428,7 @@ DEALINGS IN THE SOFTWARE. httparse 1.8.0 - MIT/Apache-2.0 https://github.com/seanmonstar/httparse -Copyright (c) 2015-2021 Sean McArthur +Copyright (c) 2015-2024 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -3649,7 +3451,7 @@ THE SOFTWARE. --------------------------------------------------------- -httpdate 1.0.2 - MIT/Apache-2.0 +httpdate 1.0.3 - MIT OR Apache-2.0 https://github.com/pyfisch/httpdate Copyright (c) 2016 Pyfisch @@ -3675,7 +3477,7 @@ THE SOFTWARE. --------------------------------------------------------- -hyper 0.14.26 - MIT +hyper 0.14.28 - MIT https://github.com/hyperium/hyper The MIT License (MIT) @@ -3729,7 +3531,7 @@ THE SOFTWARE. --------------------------------------------------------- -iana-time-zone 0.1.57 - MIT OR Apache-2.0 +iana-time-zone 0.1.60 - MIT OR Apache-2.0 https://github.com/strawlab/iana-time-zone Copyright (c) 2020 Andrew D. Straw @@ -3761,7 +3563,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -iana-time-zone-haiku 0.1.1 - MIT OR Apache-2.0 +iana-time-zone-haiku 0.1.2 - MIT OR Apache-2.0 https://github.com/strawlab/iana-time-zone Copyright (c) 2020 Andrew D. Straw @@ -3793,7 +3595,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -idna 0.3.0 - MIT OR Apache-2.0 +idna 0.5.0 - MIT OR Apache-2.0 https://github.com/servo/rust-url/ Copyright (c) 2013-2022 The rust-url developers @@ -3825,8 +3627,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -indexmap 1.9.1 - Apache-2.0 OR MIT -indexmap 2.1.0 - Apache-2.0 OR MIT +indexmap 1.9.3 - Apache-2.0 OR MIT +indexmap 2.2.6 - Apache-2.0 OR MIT https://github.com/indexmap-rs/indexmap Copyright (c) 2016--2017 @@ -3858,7 +3660,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -indicatif 0.17.4 - MIT +indicatif 0.17.8 - MIT https://github.com/console-rs/indicatif The MIT License (MIT) @@ -3940,7 +3742,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -instant 0.1.12 - BSD-3-Clause +instant 0.1.13 - BSD-3-Clause https://github.com/sebcrozet/instant Copyright (c) 2019, Sébastien Crozet @@ -4004,7 +3806,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -ipnet 2.5.0 - MIT OR Apache-2.0 +ipnet 2.9.0 - MIT OR Apache-2.0 https://github.com/krisprice/ipnet Copyright 2017 Juniper Networks, Inc. @@ -4046,38 +3848,6 @@ SOFTWARE. --------------------------------------------------------- -is-terminal 0.4.7 - MIT -https://github.com/sunfishcode/is-terminal - -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - is-wsl 0.4.0 - MIT https://github.com/TheLarkInn/is-wsl @@ -4106,7 +3876,33 @@ SOFTWARE. --------------------------------------------------------- -itoa 1.0.4 - MIT OR Apache-2.0 +is_terminal_polyfill 1.70.0 - MIT OR Apache-2.0 +https://github.com/polyfill-rs/is_terminal_polyfill + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +itoa 1.0.11 - MIT OR Apache-2.0 https://github.com/dtolnay/itoa Permission is hereby granted, free of charge, to any @@ -4136,7 +3932,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -js-sys 0.3.60 - MIT/Apache-2.0 +js-sys 0.3.69 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys Copyright (c) 2014 Alex Crichton @@ -4168,7 +3964,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -keyring 2.0.3 - MIT OR Apache-2.0 +keyring 2.3.3 - MIT OR Apache-2.0 https://github.com/hwchen/keyring-rs Copyright (c) 2016 keyring Developers @@ -4232,7 +4028,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -libc 0.2.153 - MIT OR Apache-2.0 +libc 0.2.155 - MIT OR Apache-2.0 https://github.com/rust-lang/libc Copyright (c) 2014-2020 The Rust Project Developers @@ -4264,7 +4060,35 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -libz-sys 1.1.12 - MIT OR Apache-2.0 +libredox 0.1.3 - MIT +https://gitlab.redox-os.org/redox-os/libredox.git + +MIT License + +Copyright (c) 2023 4lDO2 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +libz-sys 1.1.16 - MIT OR Apache-2.0 https://github.com/rust-lang/libz-sys Copyright (c) 2014 Alex Crichton @@ -4297,37 +4121,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -link-cplusplus 1.0.9 - MIT OR Apache-2.0 -https://github.com/dtolnay/link-cplusplus - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - -linux-keyutils 0.2.3 - Apache-2.0 OR MIT +linux-keyutils 0.2.4 - Apache-2.0 OR MIT https://github.com/landhb/linux-keyutils Licensed under either of the following at your discretion: @@ -4359,6 +4153,7 @@ additional terms or conditions. --------------------------------------------------------- linux-raw-sys 0.3.8 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +linux-raw-sys 0.4.14 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT https://github.com/sunfishcode/linux-raw-sys Permission is hereby granted, free of charge, to any @@ -4388,7 +4183,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -lock_api 0.4.9 - MIT OR Apache-2.0 +lock_api 0.4.12 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot Copyright (c) 2016 The Rust Project Developers @@ -4420,7 +4215,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -log 0.4.18 - MIT OR Apache-2.0 +log 0.4.21 - MIT OR Apache-2.0 https://github.com/rust-lang/log Copyright (c) 2014 The Rust Project Developers @@ -4466,7 +4261,7 @@ The following two notices apply to every file of the project. ## The Apache License ``` -Copyright 2015–2019 The md5 Developers +Copyright 2015–2024 The md5 Developers Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the @@ -4483,7 +4278,7 @@ specific language governing permissions and limitations under the License. ## The MIT License ``` -Copyright 2015–2019 The md5 Developers +Copyright 2015–2024 The md5 Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -4506,7 +4301,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -memchr 2.5.0 - Unlicense/MIT +memchr 2.7.2 - Unlicense OR MIT https://github.com/BurntSushi/memchr The MIT License (MIT) @@ -4535,6 +4330,7 @@ THE SOFTWARE. --------------------------------------------------------- memoffset 0.7.1 - MIT +memoffset 0.9.1 - MIT https://github.com/Gilnaa/memoffset The MIT License (MIT) @@ -4562,7 +4358,7 @@ SOFTWARE. --------------------------------------------------------- -mime 0.3.16 - MIT/Apache-2.0 +mime 0.3.17 - MIT OR Apache-2.0 https://github.com/hyperium/mime Copyright (c) 2014-2019 Sean McArthur @@ -4588,7 +4384,7 @@ THE SOFTWARE. --------------------------------------------------------- -miniz_oxide 0.7.1 - MIT OR Zlib OR Apache-2.0 +miniz_oxide 0.7.3 - MIT OR Zlib OR Apache-2.0 https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide This library (excluding the miniz C code used for tests) is licensed under the MIT license. The library is based on the miniz C library, of which the parts used are dual-licensed under the [MIT license](https://github.com/Frommi/miniz_oxide/blob/master/miniz/miniz.c#L1) and also the [unlicense](https://github.com/Frommi/miniz_oxide/blob/master/miniz/miniz.c#L577). @@ -4651,7 +4447,7 @@ SOFTWARE. --------------------------------------------------------- -nix 0.26.2 - MIT +nix 0.26.4 - MIT https://github.com/nix-rust/nix The MIT License (MIT) @@ -4679,7 +4475,7 @@ THE SOFTWARE. --------------------------------------------------------- -ntapi 0.4.0 - Apache-2.0 OR MIT +ntapi 0.4.1 - Apache-2.0 OR MIT https://github.com/MSxDOS/ntapi Permission is hereby granted, free of charge, to any person obtaining a copy @@ -4703,7 +4499,7 @@ SOFTWARE. --------------------------------------------------------- -num 0.4.0 - MIT OR Apache-2.0 +num 0.4.3 - MIT OR Apache-2.0 https://github.com/rust-num/num Copyright (c) 2014 The Rust Project Developers @@ -4735,7 +4531,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-bigint 0.4.3 - MIT OR Apache-2.0 +num-bigint 0.4.5 - MIT OR Apache-2.0 https://github.com/rust-num/num-bigint Copyright (c) 2014 The Rust Project Developers @@ -4767,7 +4563,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-complex 0.4.2 - MIT OR Apache-2.0 +num-complex 0.4.6 - MIT OR Apache-2.0 https://github.com/rust-num/num-complex Copyright (c) 2014 The Rust Project Developers @@ -4799,7 +4595,33 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-integer 0.1.45 - MIT OR Apache-2.0 +num-conv 0.1.0 - MIT OR Apache-2.0 +https://github.com/jhpratt/num-conv + +Copyright (c) 2023 Jacob Pratt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +num-integer 0.1.46 - MIT OR Apache-2.0 https://github.com/rust-num/num-integer Copyright (c) 2014 The Rust Project Developers @@ -4831,7 +4653,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-iter 0.1.43 - MIT OR Apache-2.0 +num-iter 0.1.45 - MIT OR Apache-2.0 https://github.com/rust-num/num-iter Copyright (c) 2014 The Rust Project Developers @@ -4863,7 +4685,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-rational 0.4.1 - MIT OR Apache-2.0 +num-rational 0.4.2 - MIT OR Apache-2.0 https://github.com/rust-num/num-rational Copyright (c) 2014 The Rust Project Developers @@ -4895,7 +4717,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-traits 0.2.15 - MIT OR Apache-2.0 +num-traits 0.2.19 - MIT OR Apache-2.0 https://github.com/rust-num/num-traits Copyright (c) 2014 The Rust Project Developers @@ -4927,7 +4749,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num_cpus 1.13.1 - MIT OR Apache-2.0 +num_cpus 1.16.0 - MIT OR Apache-2.0 https://github.com/seanmonstar/num_cpus Copyright (c) 2015 @@ -4981,7 +4803,39 @@ SOFTWARE. --------------------------------------------------------- -once_cell 1.17.2 - MIT OR Apache-2.0 +object 0.32.2 - Apache-2.0 OR MIT +https://github.com/gimli-rs/object + +Copyright (c) 2015 The Gimli Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +once_cell 1.19.0 - MIT OR Apache-2.0 https://github.com/matklad/once_cell Permission is hereby granted, free of charge, to any @@ -5011,7 +4865,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -open 4.1.0 - MIT +open 4.2.0 - MIT https://github.com/Byron/open-rs The MIT License (MIT) @@ -5042,7 +4896,7 @@ OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -openssl 0.10.60 - Apache-2.0 +openssl 0.10.64 - Apache-2.0 https://github.com/sfackler/rust-openssl Copyright 2011-2017 Google Inc. @@ -5064,7 +4918,7 @@ limitations under the License. --------------------------------------------------------- -openssl-macros 0.1.0 - MIT/Apache-2.0 +openssl-macros 0.1.1 - MIT/Apache-2.0 This software is released under the MIT license: @@ -5121,7 +4975,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -openssl-sys 0.9.96 - MIT +openssl-sys 0.9.102 - MIT https://github.com/sfackler/rust-openssl The MIT License (MIT) @@ -5346,7 +5200,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -5554,7 +5408,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -5762,7 +5616,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -6189,7 +6043,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -os_info 3.7.0 - MIT +os_info 3.8.2 - MIT https://github.com/stanislav-tkach/os_info The MIT License (MIT) @@ -6217,7 +6071,7 @@ SOFTWARE. --------------------------------------------------------- -parking 2.0.0 - Apache-2.0 OR MIT +parking 2.2.0 - Apache-2.0 OR MIT https://github.com/smol-rs/parking Permission is hereby granted, free of charge, to any @@ -6247,7 +6101,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -parking_lot 0.12.1 - MIT OR Apache-2.0 +parking_lot 0.12.2 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot Copyright (c) 2016 The Rust Project Developers @@ -6279,7 +6133,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -parking_lot_core 0.9.3 - MIT OR Apache-2.0 +parking_lot_core 0.9.10 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot Copyright (c) 2016 The Rust Project Developers @@ -6311,7 +6165,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -paste 1.0.9 - MIT OR Apache-2.0 +paste 1.0.15 - MIT OR Apache-2.0 https://github.com/dtolnay/paste Permission is hereby granted, free of charge, to any @@ -6371,7 +6225,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -percent-encoding 2.2.0 - MIT OR Apache-2.0 +percent-encoding 2.3.1 - MIT OR Apache-2.0 https://github.com/servo/rust-url/ Copyright (c) 2013-2022 The rust-url developers @@ -6403,7 +6257,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pin-project 1.1.0 - Apache-2.0 OR MIT +pin-project 1.1.5 - Apache-2.0 OR MIT https://github.com/taiki-e/pin-project Permission is hereby granted, free of charge, to any @@ -6433,7 +6287,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pin-project-internal 1.1.0 - Apache-2.0 OR MIT +pin-project-internal 1.1.5 - Apache-2.0 OR MIT https://github.com/taiki-e/pin-project Permission is hereby granted, free of charge, to any @@ -6463,7 +6317,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pin-project-lite 0.2.9 - Apache-2.0 OR MIT +pin-project-lite 0.2.14 - Apache-2.0 OR MIT https://github.com/taiki-e/pin-project-lite Permission is hereby granted, free of charge, to any @@ -6525,7 +6379,37 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pkg-config 0.3.25 - MIT OR Apache-2.0 +piper 0.2.2 - MIT OR Apache-2.0 +https://github.com/smol-rs/piper + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +pkg-config 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/pkg-config-rs Copyright (c) 2014 Alex Crichton @@ -6557,7 +6441,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -polling 2.3.0 - Apache-2.0 OR MIT +polling 2.8.0 - Apache-2.0 OR MIT +polling 3.7.0 - Apache-2.0 OR MIT https://github.com/smol-rs/polling Permission is hereby granted, free of charge, to any @@ -6587,7 +6472,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -portable-atomic 1.3.3 - Apache-2.0 OR MIT +portable-atomic 1.6.0 - Apache-2.0 OR MIT https://github.com/taiki-e/portable-atomic Permission is hereby granted, free of charge, to any @@ -6617,7 +6502,33 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -ppv-lite86 0.2.16 - MIT/Apache-2.0 +powerfmt 0.2.0 - MIT OR Apache-2.0 +https://github.com/jhpratt/powerfmt + +Copyright (c) 2023 Jacob Pratt et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +ppv-lite86 0.2.17 - MIT/Apache-2.0 https://github.com/cryptocorrosion/cryptocorrosion Copyright (c) 2019 The CryptoCorrosion Contributors @@ -6649,7 +6560,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -proc-macro-crate 1.2.1 - Apache-2.0/MIT +proc-macro-crate 1.3.1 - MIT OR Apache-2.0 https://github.com/bkchr/proc-macro-crate Permission is hereby granted, free of charge, to any @@ -6679,7 +6590,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -proc-macro2 1.0.59 - MIT OR Apache-2.0 +proc-macro2 1.0.83 - MIT OR Apache-2.0 https://github.com/dtolnay/proc-macro2 Permission is hereby granted, free of charge, to any @@ -6709,7 +6620,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -quote 1.0.28 - MIT OR Apache-2.0 +quote 1.0.36 - MIT OR Apache-2.0 https://github.com/dtolnay/quote Permission is hereby granted, free of charge, to any @@ -6874,8 +6785,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -redox_syscall 0.2.16 - MIT -redox_syscall 0.3.5 - MIT +redox_syscall 0.4.1 - MIT +redox_syscall 0.5.1 - MIT https://github.com/redox-os/syscall Copyright (c) 2017 Redox OS Developers @@ -6904,7 +6815,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -redox_users 0.4.3 - MIT +redox_users 0.4.5 - MIT https://gitlab.redox-os.org/redox-os/users The MIT License (MIT) @@ -6932,7 +6843,7 @@ SOFTWARE. --------------------------------------------------------- -regex 1.8.3 - MIT OR Apache-2.0 +regex 1.10.4 - MIT OR Apache-2.0 https://github.com/rust-lang/regex Copyright (c) 2014 The Rust Project Developers @@ -6964,7 +6875,39 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -regex-syntax 0.7.2 - MIT OR Apache-2.0 +regex-automata 0.4.6 - MIT OR Apache-2.0 +https://github.com/rust-lang/regex/tree/master/regex-automata + +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +regex-syntax 0.8.3 - MIT OR Apache-2.0 https://github.com/rust-lang/regex/tree/master/regex-syntax Copyright (c) 2014 The Rust Project Developers @@ -6996,7 +6939,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -reqwest 0.11.22 - MIT OR Apache-2.0 +reqwest 0.11.27 - MIT OR Apache-2.0 https://github.com/seanmonstar/reqwest Copyright (c) 2016 Sean McArthur @@ -7022,7 +6965,7 @@ THE SOFTWARE. --------------------------------------------------------- -rmp 0.8.11 - MIT +rmp 0.8.14 - MIT https://github.com/3Hren/msgpack-rust MIT License @@ -7050,7 +6993,7 @@ SOFTWARE. --------------------------------------------------------- -rmp-serde 1.1.1 - MIT +rmp-serde 1.3.0 - MIT https://github.com/3Hren/msgpack-rust MIT License @@ -7078,7 +7021,7 @@ SOFTWARE. --------------------------------------------------------- -russh 6a15199c784c0b6d171a6fec09ed730a5cd1350d +russh fd4f608a83753f9f3e137f95600faffede71cf65 https://github.com/microsoft/vscode-russh Apache License @@ -7286,7 +7229,7 @@ Apache License --------------------------------------------------------- -russh-cryptovec 6a15199c784c0b6d171a6fec09ed730a5cd1350d +russh-cryptovec fd4f608a83753f9f3e137f95600faffede71cf65 https://github.com/microsoft/vscode-russh Apache License @@ -7494,7 +7437,7 @@ Apache License --------------------------------------------------------- -russh-keys 6a15199c784c0b6d171a6fec09ed730a5cd1350d +russh-keys fd4f608a83753f9f3e137f95600faffede71cf65 https://github.com/microsoft/vscode-russh Apache License @@ -7702,7 +7645,40 @@ Apache License --------------------------------------------------------- -rustix 0.37.25 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +rustc-demangle 0.1.24 - MIT/Apache-2.0 +https://github.com/rust-lang/rustc-demangle + +Copyright (c) 2014 Alex Crichton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE 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. +--------------------------------------------------------- + +--------------------------------------------------------- + +rustix 0.37.27 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +rustix 0.38.34 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT https://github.com/bytecodealliance/rustix Permission is hereby granted, free of charge, to any @@ -7732,7 +7708,23 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -ryu 1.0.11 - Apache-2.0 OR BSL-1.0 +rustls-pemfile 1.0.4 - Apache-2.0 OR ISC OR MIT +https://github.com/rustls/pemfile + +rustls-pemfile is distributed under the following three licenses: + +- Apache License version 2.0. +- MIT license. +- ISC license. + +These are included as LICENSE-APACHE, LICENSE-MIT and LICENSE-ISC +respectively. You may use this software under the terms of any +of these licenses, at your option. +--------------------------------------------------------- + +--------------------------------------------------------- + +ryu 1.0.18 - Apache-2.0 OR BSL-1.0 https://github.com/dtolnay/ryu @@ -7752,7 +7744,7 @@ be dual licensed as above, without any additional terms or conditions. --------------------------------------------------------- -schannel 0.1.20 - MIT +schannel 0.1.23 - MIT https://github.com/steffengy/schannel-rs The MIT License (MIT) @@ -7768,7 +7760,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -scopeguard 1.1.0 - MIT/Apache-2.0 +scopeguard 1.2.0 - MIT OR Apache-2.0 https://github.com/bluss/scopeguard Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers @@ -7800,36 +7792,6 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -scratch 1.0.7 - MIT OR Apache-2.0 -https://github.com/dtolnay/scratch - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - secret-service 3.0.1 - MIT OR Apache-2.0 https://github.com/hwchen/secret-service-rs @@ -7862,7 +7824,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -security-framework 2.7.0 - MIT OR Apache-2.0 +security-framework 2.11.0 - MIT OR Apache-2.0 https://github.com/kornelski/rust-security-framework The MIT License (MIT) @@ -7889,7 +7851,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -security-framework-sys 2.6.1 - MIT OR Apache-2.0 +security-framework-sys 2.11.0 - MIT OR Apache-2.0 https://github.com/kornelski/rust-security-framework The MIT License (MIT) @@ -7916,7 +7878,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde 1.0.163 - MIT OR Apache-2.0 +serde 1.0.202 - MIT OR Apache-2.0 https://github.com/serde-rs/serde Permission is hereby granted, free of charge, to any @@ -7946,7 +7908,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_bytes 0.11.9 - MIT OR Apache-2.0 +serde_bytes 0.11.14 - MIT OR Apache-2.0 https://github.com/serde-rs/bytes Permission is hereby granted, free of charge, to any @@ -7976,7 +7938,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_derive 1.0.163 - MIT OR Apache-2.0 +serde_derive 1.0.202 - MIT OR Apache-2.0 https://github.com/serde-rs/serde Permission is hereby granted, free of charge, to any @@ -8006,7 +7968,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_json 1.0.96 - MIT OR Apache-2.0 +serde_json 1.0.117 - MIT OR Apache-2.0 https://github.com/serde-rs/json Permission is hereby granted, free of charge, to any @@ -8036,7 +7998,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_repr 0.1.9 - MIT OR Apache-2.0 +serde_repr 0.1.19 - MIT OR Apache-2.0 https://github.com/dtolnay/serde-repr Permission is hereby granted, free of charge, to any @@ -8098,7 +8060,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -sha1 0.10.5 - MIT OR Apache-2.0 +sha1 0.10.6 - MIT OR Apache-2.0 https://github.com/RustCrypto/hashes All crates in this repository are licensed under either of @@ -8138,6 +8100,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`md5`]: ./md5 [`ripemd`]: ./ripemd [`sha1`]: ./sha1 +[`sha1-checked`]: ./sha1-checked [`sha2`]: ./sha2 [`sha3`]: ./sha3 [`shabal`]: ./shabal @@ -8179,6 +8142,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [MD5]: https://en.wikipedia.org/wiki/MD5 [RIPEMD]: https://en.wikipedia.org/wiki/RIPEMD [SHA-1]: https://en.wikipedia.org/wiki/SHA-1 +[SHA-1 Checked]: https://github.com/cr-marcstevens/sha1collisiondetection [SHA-2]: https://en.wikipedia.org/wiki/SHA-2 [SHA-3]: https://en.wikipedia.org/wiki/SHA-3 [SHABAL]: https://www.cs.rit.edu/~ark/20090927/Round2Candidates/Shabal.pdf @@ -8191,7 +8155,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -sha2 0.10.6 - MIT OR Apache-2.0 +sha2 0.10.8 - MIT OR Apache-2.0 https://github.com/RustCrypto/hashes All crates in this repository are licensed under either of @@ -8231,6 +8195,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`md5`]: ./md5 [`ripemd`]: ./ripemd [`sha1`]: ./sha1 +[`sha1-checked`]: ./sha1-checked [`sha2`]: ./sha2 [`sha3`]: ./sha3 [`shabal`]: ./shabal @@ -8272,6 +8237,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [MD5]: https://en.wikipedia.org/wiki/MD5 [RIPEMD]: https://en.wikipedia.org/wiki/RIPEMD [SHA-1]: https://en.wikipedia.org/wiki/SHA-1 +[SHA-1 Checked]: https://github.com/cr-marcstevens/sha1collisiondetection [SHA-2]: https://en.wikipedia.org/wiki/SHA-2 [SHA-3]: https://en.wikipedia.org/wiki/SHA-3 [SHABAL]: https://www.cs.rit.edu/~ark/20090927/Round2Candidates/Shabal.pdf @@ -8348,7 +8314,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -signal-hook 0.3.15 - Apache-2.0/MIT +signal-hook-registry 1.4.2 - Apache-2.0/MIT https://github.com/vorner/signal-hook Copyright (c) 2017 tokio-jsonrpc developers @@ -8380,39 +8346,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -signal-hook-registry 1.4.0 - Apache-2.0/MIT -https://github.com/vorner/signal-hook - -Copyright (c) 2017 tokio-jsonrpc developers - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. ---------------------------------------------------------- - ---------------------------------------------------------- - -slab 0.4.7 - MIT +slab 0.4.9 - MIT https://github.com/tokio-rs/slab The MIT License (MIT) @@ -8446,7 +8380,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -smallvec 1.10.0 - MIT OR Apache-2.0 +smallvec 1.13.2 - MIT OR Apache-2.0 https://github.com/servo/rust-smallvec Copyright (c) 2018 The Servo Project Developers @@ -8478,7 +8412,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -socket2 0.4.9 - MIT OR Apache-2.0 +socket2 0.4.10 - MIT OR Apache-2.0 +socket2 0.5.7 - MIT OR Apache-2.0 https://github.com/rust-lang/socket2 Copyright (c) 2014 Alex Crichton @@ -8538,7 +8473,7 @@ SOFTWARE. --------------------------------------------------------- -strsim 0.10.0 - MIT +strsim 0.11.1 - MIT https://github.com/rapidfuzz/strsim-rs The MIT License (MIT) @@ -8568,10 +8503,11 @@ SOFTWARE. --------------------------------------------------------- -subtle 2.4.1 - BSD-3-Clause +subtle 2.5.0 - BSD-3-Clause https://github.com/dalek-cryptography/subtle Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -8603,8 +8539,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------------------------------------------- -syn 1.0.103 - MIT OR Apache-2.0 -syn 2.0.18 - MIT OR Apache-2.0 +syn 1.0.109 - MIT OR Apache-2.0 +syn 2.0.65 - MIT OR Apache-2.0 https://github.com/dtolnay/syn Permission is hereby granted, free of charge, to any @@ -8634,7 +8570,190 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -sysinfo 0.29.0 - MIT +sync_wrapper 0.1.2 - Apache-2.0 +https://github.com/Actyx/sync_wrapper + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +--------------------------------------------------------- + +--------------------------------------------------------- + +sysinfo 0.29.11 - MIT https://github.com/GuillaumeGomez/sysinfo The MIT License (MIT) @@ -8726,7 +8845,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tar 0.4.38 - MIT/Apache-2.0 +tar 0.4.40 - MIT/Apache-2.0 https://github.com/alexcrichton/tar-rs Copyright (c) 2014 Alex Crichton @@ -8758,7 +8877,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tempfile 3.5.0 - MIT OR Apache-2.0 +tempfile 3.10.1 - MIT OR Apache-2.0 https://github.com/Stebalien/tempfile Copyright (c) 2015 Steven Allen @@ -8790,35 +8909,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -termcolor 1.2.0 - Unlicense OR MIT -https://github.com/BurntSushi/termcolor - -The MIT License (MIT) - -Copyright (c) 2015 Andrew Gallant - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -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. ---------------------------------------------------------- - ---------------------------------------------------------- - -thiserror 1.0.40 - MIT OR Apache-2.0 +thiserror 1.0.61 - MIT OR Apache-2.0 https://github.com/dtolnay/thiserror Permission is hereby granted, free of charge, to any @@ -8848,7 +8939,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -thiserror-impl 1.0.40 - MIT OR Apache-2.0 +thiserror-impl 1.0.61 - MIT OR Apache-2.0 https://github.com/dtolnay/thiserror Permission is hereby granted, free of charge, to any @@ -8878,7 +8969,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -time 0.3.21 - MIT OR Apache-2.0 +time 0.3.36 - MIT OR Apache-2.0 https://github.com/time-rs/time Copyright (c) 2024 Jacob Pratt et al. @@ -8904,7 +8995,7 @@ SOFTWARE. --------------------------------------------------------- -time-core 0.1.1 - MIT OR Apache-2.0 +time-core 0.1.2 - MIT OR Apache-2.0 https://github.com/time-rs/time Copyright (c) 2024 Jacob Pratt et al. @@ -8942,7 +9033,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -tinyvec_macros 0.1.0 - MIT OR Apache-2.0 OR Zlib +tinyvec_macros 0.1.1 - MIT OR Apache-2.0 OR Zlib https://github.com/Soveu/tinyvec_macros MIT License @@ -8970,70 +9061,58 @@ SOFTWARE. --------------------------------------------------------- -tokio 1.28.2 - MIT +tokio 1.37.0 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. --------------------------------------------------------- --------------------------------------------------------- -tokio-macros 2.1.0 - MIT +tokio-macros 2.2.0 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. --------------------------------------------------------- --------------------------------------------------------- @@ -9072,36 +9151,30 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tokio-stream 0.1.11 - MIT +tokio-stream 0.1.15 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. --------------------------------------------------------- --------------------------------------------------------- @@ -9135,41 +9208,61 @@ THE SOFTWARE. --------------------------------------------------------- -tokio-util 0.7.8 - MIT +tokio-util 0.7.11 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE 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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. --------------------------------------------------------- --------------------------------------------------------- -toml 0.5.9 - MIT/Apache-2.0 +toml_datetime 0.6.6 - MIT OR Apache-2.0 +https://github.com/toml-rs/toml + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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. +--------------------------------------------------------- + +--------------------------------------------------------- + +toml_edit 0.19.15 - MIT OR Apache-2.0 https://github.com/toml-rs/toml Copyright (c) Individual contributors @@ -9229,7 +9322,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tracing 0.1.37 - MIT +tracing 0.1.40 - MIT https://github.com/tokio-rs/tracing The MIT License (MIT) @@ -9263,7 +9356,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tracing-attributes 0.1.23 - MIT +tracing-attributes 0.1.27 - MIT https://github.com/tokio-rs/tracing The MIT License (MIT) @@ -9297,7 +9390,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tracing-core 0.1.30 - MIT +tracing-core 0.1.32 - MIT https://github.com/tokio-rs/tracing The MIT License (MIT) @@ -9331,7 +9424,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -try-lock 0.2.3 - MIT +try-lock 0.2.5 - MIT https://github.com/seanmonstar/try-lock The MIT License (MIT) @@ -9415,7 +9508,7 @@ MIT License --------------------------------------------------------- -typenum 1.15.0 - MIT OR Apache-2.0 +typenum 1.17.0 - MIT OR Apache-2.0 https://github.com/paholg/typenum MIT OR Apache-2.0 @@ -9423,7 +9516,7 @@ MIT OR Apache-2.0 --------------------------------------------------------- -uds_windows 1.0.2 - MIT +uds_windows 1.1.0 - MIT https://github.com/haraldh/rust_uds_windows MIT License @@ -9451,7 +9544,7 @@ MIT License --------------------------------------------------------- -unicode-bidi 0.3.8 - MIT OR Apache-2.0 +unicode-bidi 0.3.15 - MIT OR Apache-2.0 https://github.com/servo/unicode-bidi Copyright (c) 2015 The Rust Project Developers @@ -9483,7 +9576,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-ident 1.0.5 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 +unicode-ident 1.0.12 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 https://github.com/dtolnay/unicode-ident Permission is hereby granted, free of charge, to any @@ -9513,7 +9606,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-normalization 0.1.22 - MIT/Apache-2.0 +unicode-normalization 0.1.23 - MIT/Apache-2.0 https://github.com/unicode-rs/unicode-normalization Copyright (c) 2015 The Rust Project Developers @@ -9545,7 +9638,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-width 0.1.10 - MIT/Apache-2.0 +unicode-width 0.1.12 - MIT OR Apache-2.0 https://github.com/unicode-rs/unicode-width Copyright (c) 2015 The Rust Project Developers @@ -9609,7 +9702,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -url 2.3.1 - MIT OR Apache-2.0 +url 2.5.0 - MIT OR Apache-2.0 https://github.com/servo/rust-url Copyright (c) 2013-2022 The rust-url developers @@ -9732,7 +9825,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -uuid 1.4.1 - Apache-2.0 OR MIT +uuid 1.8.0 - Apache-2.0 OR MIT https://github.com/uuid-rs/uuid Copyright (c) 2014 The Rust Project Developers @@ -9823,7 +9916,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -waker-fn 1.1.0 - Apache-2.0 OR MIT +waker-fn 1.2.0 - Apache-2.0 OR MIT https://github.com/smol-rs/waker-fn Permission is hereby granted, free of charge, to any @@ -9853,7 +9946,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -want 0.3.0 - MIT +want 0.3.1 - MIT https://github.com/seanmonstar/want The MIT License (MIT) @@ -9883,7 +9976,7 @@ THE SOFTWARE. wasi 0.11.0+wasi-snapshot-preview1 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT wasi 0.9.0+wasi-snapshot-preview1 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT -https://github.com/bytecodealliance/wasi +https://github.com/bytecodealliance/wasi-rs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9912,7 +10005,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen 0.2.83 - MIT/Apache-2.0 +wasm-bindgen 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen Copyright (c) 2014 Alex Crichton @@ -9944,7 +10037,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-backend 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-backend 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend Copyright (c) 2014 Alex Crichton @@ -9976,7 +10069,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-futures 0.4.33 - MIT/Apache-2.0 +wasm-bindgen-futures 0.4.42 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures Copyright (c) 2014 Alex Crichton @@ -10008,7 +10101,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-macro 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-macro 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro Copyright (c) 2014 Alex Crichton @@ -10040,7 +10133,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-macro-support 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-macro-support 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support Copyright (c) 2014 Alex Crichton @@ -10072,7 +10165,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-shared 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-shared 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared Copyright (c) 2014 Alex Crichton @@ -10104,7 +10197,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-streams 0.3.0 - MIT OR Apache-2.0 +wasm-streams 0.4.0 - MIT OR Apache-2.0 https://github.com/MattiasBuelens/wasm-streams/ Permission is hereby granted, free of charge, to any @@ -10134,7 +10227,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -web-sys 0.3.60 - MIT/Apache-2.0 +web-sys 0.3.69 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys Copyright (c) 2014 Alex Crichton @@ -10166,34 +10259,6 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wepoll-ffi 0.1.2 - MIT OR Apache-2.0 OR BSD-2-Clause -https://github.com/aclysma/wepoll-ffi - -MIT License - -Copyright (c) 2019-2020 Philip Degarmo and other wepoll-ffi contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -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. ---------------------------------------------------------- - ---------------------------------------------------------- - winapi 0.3.9 - MIT/Apache-2.0 https://github.com/retep998/winapi-rs @@ -10246,34 +10311,6 @@ SOFTWARE. --------------------------------------------------------- -winapi-util 0.1.5 - Unlicense/MIT -https://github.com/BurntSushi/winapi-util - -The MIT License (MIT) - -Copyright (c) 2017 Andrew Gallant - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -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. ---------------------------------------------------------- - ---------------------------------------------------------- - winapi-x86_64-pc-windows-gnu 0.4.0 - MIT/Apache-2.0 https://github.com/retep998/winapi-rs @@ -10300,7 +10337,7 @@ SOFTWARE. --------------------------------------------------------- -windows 0.48.0 - MIT OR Apache-2.0 +windows-core 0.52.0 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10328,9 +10365,8 @@ MIT License --------------------------------------------------------- -windows-sys 0.36.1 - MIT OR Apache-2.0 -windows-sys 0.45.0 - MIT OR Apache-2.0 windows-sys 0.48.0 - MIT OR Apache-2.0 +windows-sys 0.52.0 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10358,8 +10394,8 @@ MIT License --------------------------------------------------------- -windows-targets 0.42.2 - MIT OR Apache-2.0 -windows-targets 0.48.0 - MIT OR Apache-2.0 +windows-targets 0.48.5 - MIT OR Apache-2.0 +windows-targets 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10387,8 +10423,8 @@ MIT License --------------------------------------------------------- -windows_aarch64_gnullvm 0.42.2 - MIT OR Apache-2.0 -windows_aarch64_gnullvm 0.48.0 - MIT OR Apache-2.0 +windows_aarch64_gnullvm 0.48.5 - MIT OR Apache-2.0 +windows_aarch64_gnullvm 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10416,9 +10452,8 @@ MIT License --------------------------------------------------------- -windows_aarch64_msvc 0.36.1 - MIT OR Apache-2.0 -windows_aarch64_msvc 0.42.2 - MIT OR Apache-2.0 -windows_aarch64_msvc 0.48.0 - MIT OR Apache-2.0 +windows_aarch64_msvc 0.48.5 - MIT OR Apache-2.0 +windows_aarch64_msvc 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10446,9 +10481,8 @@ MIT License --------------------------------------------------------- -windows_i686_gnu 0.36.1 - MIT OR Apache-2.0 -windows_i686_gnu 0.42.2 - MIT OR Apache-2.0 -windows_i686_gnu 0.48.0 - MIT OR Apache-2.0 +windows_i686_gnu 0.48.5 - MIT OR Apache-2.0 +windows_i686_gnu 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10476,9 +10510,7 @@ MIT License --------------------------------------------------------- -windows_i686_msvc 0.36.1 - MIT OR Apache-2.0 -windows_i686_msvc 0.42.2 - MIT OR Apache-2.0 -windows_i686_msvc 0.48.0 - MIT OR Apache-2.0 +windows_i686_gnullvm 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10506,9 +10538,8 @@ MIT License --------------------------------------------------------- -windows_x86_64_gnu 0.36.1 - MIT OR Apache-2.0 -windows_x86_64_gnu 0.42.2 - MIT OR Apache-2.0 -windows_x86_64_gnu 0.48.0 - MIT OR Apache-2.0 +windows_i686_msvc 0.48.5 - MIT OR Apache-2.0 +windows_i686_msvc 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10536,8 +10567,8 @@ MIT License --------------------------------------------------------- -windows_x86_64_gnullvm 0.42.2 - MIT OR Apache-2.0 -windows_x86_64_gnullvm 0.48.0 - MIT OR Apache-2.0 +windows_x86_64_gnu 0.48.5 - MIT OR Apache-2.0 +windows_x86_64_gnu 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10565,9 +10596,8 @@ MIT License --------------------------------------------------------- -windows_x86_64_msvc 0.36.1 - MIT OR Apache-2.0 -windows_x86_64_msvc 0.42.2 - MIT OR Apache-2.0 -windows_x86_64_msvc 0.48.0 - MIT OR Apache-2.0 +windows_x86_64_gnullvm 0.48.5 - MIT OR Apache-2.0 +windows_x86_64_gnullvm 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10595,7 +10625,36 @@ MIT License --------------------------------------------------------- -winnow 0.4.1 - MIT +windows_x86_64_msvc 0.48.5 - MIT OR Apache-2.0 +windows_x86_64_msvc 0.52.5 - MIT OR Apache-2.0 +https://github.com/microsoft/windows-rs + +MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + 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 +--------------------------------------------------------- + +--------------------------------------------------------- + +winnow 0.5.40 - MIT https://github.com/winnow-rs/winnow The MIT License (MIT) @@ -10651,7 +10710,7 @@ THE SOFTWARE. --------------------------------------------------------- -xattr 0.2.3 - MIT/Apache-2.0 +xattr 1.3.1 - MIT/Apache-2.0 https://github.com/Stebalien/xattr Copyright (c) 2015 Steven Allen @@ -10683,7 +10742,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -xdg-home 1.0.0 - MIT +xdg-home 1.1.0 - MIT https://github.com/zeenix/xdg-home The MIT License (MIT) @@ -10729,11 +10788,13 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -zbus 3.13.1 - MIT +zbus 3.15.2 - MIT https://github.com/dbus2/zbus/ The MIT License (MIT) +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the @@ -10761,11 +10822,13 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -zbus_macros 3.13.1 - MIT +zbus_macros 3.15.2 - MIT https://github.com/dbus2/zbus/ The MIT License (MIT) +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the @@ -10793,11 +10856,13 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -zbus_names 2.5.1 - MIT +zbus_names 2.6.1 - MIT https://github.com/dbus2/zbus/ The MIT License (MIT) +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the @@ -10825,7 +10890,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -zeroize 1.3.0 - Apache-2.0 OR MIT +zeroize 1.7.0 - Apache-2.0 OR MIT https://github.com/RustCrypto/utils/tree/master/zeroize All crates licensed under either of @@ -10880,7 +10945,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- zip 0.6.6 - MIT -https://github.com/zip-rs/zip +https://github.com/zip-rs/zip2 The MIT License (MIT) @@ -10903,15 +10968,20 @@ 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. + +Some files in the "tests/data" subdirectory of this repository are under other +licences; see files named LICENSE.*.txt for details. --------------------------------------------------------- --------------------------------------------------------- -zvariant 3.14.0 - MIT +zvariant 3.15.2 - MIT https://github.com/dbus2/zbus/ The MIT License (MIT) +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the @@ -10939,11 +11009,13 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -zvariant_derive 3.14.0 - MIT +zvariant_derive 3.15.2 - MIT https://github.com/dbus2/zbus/ The MIT License (MIT) +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the @@ -10976,6 +11048,8 @@ https://github.com/dbus2/zbus/ The MIT License (MIT) +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the diff --git a/cli/src/async_pipe.rs b/cli/src/async_pipe.rs index e9b710c1d68..78aed6fe3e7 100644 --- a/cli/src/async_pipe.rs +++ b/cli/src/async_pipe.rs @@ -227,7 +227,7 @@ impl hyper::server::accept::Accept for PollableAsyncListener { } } -/// Gets a random name for a pipe/socket on the paltform +/// Gets a random name for a pipe/socket on the platform pub fn get_socket_name() -> PathBuf { cfg_if::cfg_if! { if #[cfg(unix)] { diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 9d5c9b73fdb..2d9162c5483 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -287,7 +287,7 @@ impl StorageImplementation for ThreadKeyringStorage { #[derive(Default)] struct KeyringStorage { - // keywring storage can be split into multiple entries due to entry length limits + // keyring storage can be split into multiple entries due to entry length limits // on Windows https://github.com/microsoft/vscode-cli/issues/358 entries: Vec, } @@ -480,6 +480,7 @@ impl Auth { &self, provider: Option, access_token: Option, + refresh_token: Option, ) -> Result { let provider = match provider { Some(p) => p, @@ -490,8 +491,12 @@ impl Auth { Some(t) => StoredCredential { provider, access_token: t, - refresh_token: None, - expires_at: None, + // if a refresh token is given, assume it's valid now but refresh it + // soon in order to get the real expiry time. + expires_at: refresh_token + .as_ref() + .map(|_| Utc::now() + chrono::Duration::minutes(5)), + refresh_token, }, None => self.do_device_code_flow_with_provider(provider).await?, }; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index dcdbd808fe7..101f1eac29f 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -64,7 +64,7 @@ pub struct IntegratedCli { pub core: CliCore, } -/// Common CLI shared between intergated and standalone interfaces. +/// Common CLI shared between integrated and standalone interfaces. #[derive(Args, Debug, Default, Clone)] pub struct CliCore { /// One or more files, folders, or URIs to open. @@ -229,9 +229,12 @@ pub struct CommandShellArgs { /// Listen on a socket instead of stdin/stdout. #[clap(long)] pub on_socket: bool, - /// Listen on a port instead of stdin/stdout. - #[clap(long, num_args = 0..=1, default_missing_value = "0")] - pub on_port: Option, + /// Listen on a host/port instead of stdin/stdout. + #[clap(long, num_args = 0..=2, default_missing_value = "0")] + pub on_port: Vec, + /// Listen on a host/port instead of stdin/stdout. + #[clap[long]] + pub on_host: Option, /// Require the given token string to be given in the handshake. #[clap(long, env = "VSCODE_CLI_REQUIRE_TOKEN")] pub require_token: Option, @@ -616,7 +619,7 @@ pub enum OutputFormat { #[derive(Args, Clone, Debug, Default)] pub struct ExistingTunnelArgs { /// Name you'd like to assign preexisting tunnel to use to connect the tunnel - /// Old option, new code sohuld just use `--name`. + /// Old option, new code should just use `--name`. #[clap(long, hide = true)] pub tunnel_name: Option, @@ -785,11 +788,14 @@ pub enum TunnelUserSubCommands { #[derive(Args, Debug, Clone)] pub struct LoginArgs { - /// An access token to store for authentication. Note: this will not be - /// refreshed if it expires! - #[clap(long, requires = "provider")] + /// An access token to store for authentication. + #[clap(long, requires = "provider", env = "VSCODE_CLI_ACCESS_TOKEN")] pub access_token: Option, + /// An access token to store for authentication. + #[clap(long, requires = "access_token", env = "VSCODE_CLI_REFRESH_TOKEN")] + pub refresh_token: Option, + /// The auth provider to use. If not provided, a prompt will be shown. #[clap(value_enum, long)] pub provider: Option, diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index fba92723426..d8d2a49bb1a 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -12,7 +12,6 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use const_format::concatcp; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -56,16 +55,9 @@ const RELEASE_CACHE_SECS: u64 = 60 * 60; /// Number of bytes for the secret keys. See workbench.ts for their usage. const SECRET_KEY_BYTES: usize = 32; /// Path to mint the key combining server and client parts. -const SECRET_KEY_MINT_PATH: &str = "/_vscode-cli/mint-key"; +const SECRET_KEY_MINT_PATH: &str = "_vscode-cli/mint-key"; /// Cookie set to the `SECRET_KEY_MINT_PATH` const PATH_COOKIE_NAME: &str = "vscode-secret-key-path"; -/// Cookie set to the `SECRET_KEY_MINT_PATH` -const PATH_COOKIE_VALUE: &str = concatcp!( - PATH_COOKIE_NAME, - "=", - SECRET_KEY_MINT_PATH, - "; SameSite=Strict; Path=/" -); /// HTTP-only cookie where the client's secret half is stored. const SECRET_KEY_COOKIE_NAME: &str = "vscode-cli-secret-half"; @@ -79,13 +71,19 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result) -> Result, Infallible> { let client_key_half = get_client_key_half(&req); - let mut res = match req.uri().path() { - SECRET_KEY_MINT_PATH => handle_secret_mint(ctx, req), - _ => handle_proxied(ctx, req).await, + let path = req.uri().path(); + + let mut res = if path.starts_with(&ctx.cm.base_path) + && path.get(ctx.cm.base_path.len()..).unwrap_or_default() == SECRET_KEY_MINT_PATH + { + handle_secret_mint(&ctx, req) + } else { + handle_proxied(&ctx, req).await }; - append_secret_headers(&mut res, &client_key_half); + append_secret_headers(&ctx.cm.base_path, &mut res, &client_key_half); Ok(res) } -async fn handle_proxied(ctx: HandleContext, req: Request) -> Response { +async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response { let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), ctx.cm.platform) { r } else { @@ -194,7 +197,7 @@ async fn handle_proxied(ctx: HandleContext, req: Request) -> Response) -> Response { +fn handle_secret_mint(ctx: &HandleContext, req: Request) -> Response { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); @@ -208,11 +211,20 @@ fn handle_secret_mint(ctx: HandleContext, req: Request) -> Response /// Appends headers to response to maintain the secret storage of the workbench: /// sets the `PATH_COOKIE_VALUE` so workbench.ts knows about the 'mint' endpoint, /// and maintains the http-only cookie the client will use for cookies. -fn append_secret_headers(res: &mut Response, client_key_half: &SecretKeyPart) { +fn append_secret_headers( + base_path: &str, + res: &mut Response, + client_key_half: &SecretKeyPart, +) { let headers = res.headers_mut(); headers.append( hyper::header::SET_COOKIE, - PATH_COOKIE_VALUE.parse().unwrap(), + format!( + "{}={}{}; SameSite=Strict; Path=/", + PATH_COOKIE_NAME, base_path, SECRET_KEY_MINT_PATH, + ) + .parse() + .unwrap(), ); headers.append( hyper::header::SET_COOKIE, @@ -496,6 +508,8 @@ struct ConnectionManager { pub platform: Platform, pub log: log::Logger, args: ServeWebArgs, + /// Server base path, ending in `/` + base_path: String, /// Cache where servers are stored cache: DownloadCache, /// Mapping of (Quality, Commit) to the state each server is in @@ -510,11 +524,24 @@ fn key_for_release(release: &Release) -> (Quality, String) { (release.quality, release.commit.clone()) } +fn normalize_base_path(p: &str) -> String { + let p = p.trim_matches('/'); + + if p.is_empty() { + return "/".to_string(); + } + + format!("/{}/", p.trim_matches('/')) +} + impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { + let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); + Arc::new(Self { platform, args, + base_path, log: ctx.log.clone(), cache: DownloadCache::new(ctx.paths.web_server_storage()), update_service: UpdateService::new( diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 59c5794bd93..2d0014bca55 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -155,36 +155,52 @@ pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Resul code_server_args: (&ctx.args).into(), }; - let mut listener: Box = match (args.on_port, args.on_socket) { - (_, true) => { - let socket = get_socket_name(); - let listener = listen_socket_rw_stream(&socket) - .await - .map_err(|e| wrap(e, "error listening on socket"))?; + let mut listener: Box = + match (args.on_port.first(), &args.on_host, args.on_socket) { + (_, _, true) => { + let socket = get_socket_name(); + let listener = listen_socket_rw_stream(&socket) + .await + .map_err(|e| wrap(e, "error listening on socket"))?; - params - .log - .result(format!("Listening on {}", socket.display())); + params + .log + .result(format!("Listening on {}", socket.display())); - Box::new(listener) - } - (Some(p), _) => { - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), p); - let listener = tokio::net::TcpListener::bind(addr) - .await - .map_err(|e| wrap(e, "error listening on port"))?; + Box::new(listener) + } + (Some(_), _, _) | (_, Some(_), _) => { + let host = args + .on_host + .as_ref() + .map(|h| h.parse().map_err(CodeError::InvalidHostAddress)) + .unwrap_or(Ok(IpAddr::V4(Ipv4Addr::LOCALHOST)))?; - params - .log - .result(format!("Listening on {}", listener.local_addr().unwrap())); + let lower_port = args.on_port.first().copied().unwrap_or_default(); + let port_no = if let Some(upper) = args.on_port.get(1) { + find_unused_port(&host, lower_port, *upper) + .await + .unwrap_or_default() + } else { + lower_port + }; - Box::new(listener) - } - _ => { - serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; - return Ok(0); - } - }; + let addr = SocketAddr::new(host, port_no); + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|e| wrap(e, "error listening on port"))?; + + params + .log + .result(format!("Listening on {}", listener.local_addr().unwrap())); + + Box::new(listener) + } + _ => { + serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; + return Ok(0); + } + }; let mut servers = FuturesUnordered::new(); @@ -209,6 +225,21 @@ pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Resul } } +async fn find_unused_port(host: &IpAddr, start_port: u16, end_port: u16) -> Option { + for port in start_port..=end_port { + if is_port_available(*host, port).await { + return Some(port); + } + } + None +} + +async fn is_port_available(host: IpAddr, port: u16) -> bool { + tokio::net::TcpListener::bind(SocketAddr::new(host, port)) + .await + .is_ok() +} + pub async fn service( ctx: CommandContext, service_args: TunnelServiceSubCommands, @@ -267,10 +298,11 @@ pub async fn service( pub async fn user(ctx: CommandContext, user_args: TunnelUserSubCommands) -> Result { let auth = Auth::new(&ctx.paths, ctx.log.clone()); match user_args { - TunnelUserSubCommands::Login(login_args) => { + TunnelUserSubCommands::Login(mut login_args) => { auth.login( login_args.provider.map(|p| p.into()), - login_args.access_token.to_owned(), + login_args.access_token.take(), + login_args.refresh_token.take(), ) .await?; } @@ -481,7 +513,12 @@ pub async fn forward( forward_args.login.provider.take(), forward_args.login.access_token.take(), ) { - auth.login(Some(p.into()), Some(at)).await?; + auth.login( + Some(p.into()), + Some(at), + forward_args.login.refresh_token.take(), + ) + .await?; } let mut tunnels = DevTunnels::new_port_forwarding(&ctx.log, auth, &ctx.paths); @@ -498,7 +535,12 @@ fn get_connection_token(tunnel: &ActiveTunnel) -> String { let mut hash = Sha256::new(); hash.update(tunnel.id.as_bytes()); let result = hash.finalize(); - b64::URL_SAFE_NO_PAD.encode(result) + let mut result = b64::URL_SAFE_NO_PAD.encode(result); + if result.starts_with('-') { + result.insert(0, 'a'); // avoid arg parsing issue + } + + result } async fn serve_with_csa( @@ -544,6 +586,16 @@ async fn serve_with_csa( match acquire_singleton(&paths.tunnel_lockfile()).await { Ok(SingletonConnection::Client(stream)) => { debug!(log, "starting as client to singleton"); + if gateway_args.name.is_some() + || !gateway_args.install_extension.is_empty() + || gateway_args.tunnel.tunnel_id.is_some() + { + warning!( + log, + "Command-line options will not be applied until the existing tunnel exits." + ); + } + let should_exit = start_singleton_client(SingletonClientArgs { log: log.clone(), shutdown: shutdown.clone(), diff --git a/cli/src/constants.rs b/cli/src/constants.rs index 6f604e8876e..1e277a89d6a 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -13,7 +13,7 @@ use crate::options::Quality; pub const CONTROL_PORT: u16 = 31545; -/// Protocol version sent to clients. This can be used to indiciate new or +/// Protocol version sent to clients. This can be used to indicate new or /// changed capabilities that clients may wish to leverage. /// 1 - Initial protocol version /// 2 - Addition of `serve.compressed` property to control whether servermsg's diff --git a/cli/src/rpc.rs b/cli/src/rpc.rs index 0972ad05475..d48d777a5dc 100644 --- a/cli/src/rpc.rs +++ b/cli/src/rpc.rs @@ -634,6 +634,7 @@ const METHOD_STREAMS_STARTED: &str = "streams_started"; const METHOD_STREAM_DATA: &str = "stream_data"; const METHOD_STREAM_ENDED: &str = "stream_ended"; +#[allow(dead_code)] // false positive trait AssertIsSync: Sync {} impl AssertIsSync for RpcDispatcher {} diff --git a/cli/src/singleton.rs b/cli/src/singleton.rs index 635c400fb0f..58c62047081 100644 --- a/cli/src/singleton.rs +++ b/cli/src/singleton.rs @@ -58,6 +58,7 @@ pub async fn acquire_singleton(lock_file: &Path) -> Result ServerBuilder<'a> { } } + /// Removes a cached server. + pub async fn evict(&self) -> Result<(), WrappedError> { + let name = get_server_folder_name( + self.server_params.release.quality, + &self.server_params.release.commit, + ); + + self.launcher_paths.server_cache.delete(&name) + } + /// Ensures the server is set up in the configured directory. pub async fn setup(&self) -> Result<(), AnyError> { debug!( @@ -407,7 +417,8 @@ impl<'a> ServerBuilder<'a> { &self.server_params.release.commit, ); - self.launcher_paths + let result = self + .launcher_paths .server_cache .create(name, |target_dir| async move { let tmpdir = @@ -433,7 +444,11 @@ impl<'a> ServerBuilder<'a> { .await?; let server_dir = target_dir.join(SERVER_FOLDER_NAME); - unzip_downloaded_release(&archive_path, &server_dir, SilentCopyProgress())?; + unzip_downloaded_release( + &archive_path, + &server_dir, + self.logger.get_download_logger("server inflate progress:"), + )?; if !skip_requirements_check().await { let output = capture_command_and_check_status( @@ -456,7 +471,12 @@ impl<'a> ServerBuilder<'a> { Ok(()) }) - .await?; + .await; + + if let Err(e) = result { + error!(self.logger, "Error installing server: {}", e); + return Err(e); + } debug!(self.logger, "Server setup complete"); @@ -469,7 +489,7 @@ impl<'a> ServerBuilder<'a> { .arg("--enable-remote-auto-shutdown") .arg(format!("--port={}", port)); - let child = self.spawn_server_process(cmd)?; + let child = self.spawn_server_process(cmd).await?; let log_file = self.get_logfile()?; let plog = self.logger.prefixed(&log::new_code_server_prefix()); @@ -477,16 +497,16 @@ impl<'a> ServerBuilder<'a> { monitor_server::(child, Some(log_file), plog, false); let port = match timeout(Duration::from_secs(8), listen_rx).await { - Err(e) => { + Err(_) => { origin.kill().await; - Err(wrap(e, "timed out looking for port")) + return Err(CodeError::ServerOriginTimeout.into()); } - Ok(Err(e)) => { + Ok(Err(s)) => { origin.kill().await; - Err(wrap(e, "server exited without writing port")) + return Err(CodeError::ServerUnexpectedExit(format!("{}", s)).into()); } - Ok(Ok(p)) => Ok(p), - }?; + Ok(Ok(p)) => p, + }; info!(self.logger, "Server started"); @@ -543,7 +563,7 @@ impl<'a> ServerBuilder<'a> { .arg("--enable-remote-auto-shutdown") .arg(format!("--socket-path={}", socket.display())); - let child = self.spawn_server_process(cmd)?; + let child = self.spawn_server_process(cmd).await?; let log_file = self.get_logfile()?; let plog = self.logger.prefixed(&log::new_code_server_prefix()); @@ -551,16 +571,16 @@ impl<'a> ServerBuilder<'a> { monitor_server::(child, Some(log_file), plog, false); let socket = match timeout(Duration::from_secs(30), listen_rx).await { - Err(e) => { + Err(_) => { origin.kill().await; - Err(wrap(e, "timed out looking for socket")) + return Err(CodeError::ServerOriginTimeout.into()); } - Ok(Err(e)) => { + Ok(Err(s)) => { origin.kill().await; - Err(wrap(e, "server exited without writing socket")) + return Err(CodeError::ServerUnexpectedExit(format!("{}", s)).into()); } - Ok(Ok(socket)) => Ok(socket), - }?; + Ok(Ok(socket)) => socket, + }; info!(self.logger, "Server started"); @@ -571,26 +591,7 @@ impl<'a> ServerBuilder<'a> { }) } - /// Starts with a given opaque set of args. Does not set up any port or - /// socket, but does return one if present, in the form of a channel. - pub async fn start_opaque_with_args( - &self, - args: &[String], - ) -> Result<(CodeServerOrigin, Receiver), AnyError> - where - M: ServerOutputMatcher, - R: 'static + Send + std::fmt::Debug, - { - let mut cmd = self.get_base_command(); - cmd.args(args); - - let child = self.spawn_server_process(cmd)?; - let plog = self.logger.prefixed(&log::new_code_server_prefix()); - - Ok(monitor_server::(child, None, plog, true)) - } - - fn spawn_server_process(&self, mut cmd: Command) -> Result { + async fn spawn_server_process(&self, mut cmd: Command) -> Result { info!(self.logger, "Starting server..."); debug!(self.logger, "Starting server with command... {:?}", cmd); @@ -602,13 +603,20 @@ impl<'a> ServerBuilder<'a> { // Original issue: https://github.com/microsoft/vscode/issues/184058 // Partial fix: https://github.com/microsoft/vscode/pull/184621 #[cfg(target_os = "windows")] - let cmd = cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW); + let cmd = cmd.creation_flags( + winapi::um::winbase::CREATE_NO_WINDOW + | winapi::um::winbase::CREATE_NEW_PROCESS_GROUP + | get_should_use_breakaway_from_job() + .await + .then_some(winapi::um::winbase::CREATE_BREAKAWAY_FROM_JOB) + .unwrap_or_default(), + ); let child = cmd .stderr(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .spawn() - .map_err(|e| wrap(e, "error spawning server"))?; + .map_err(|e| CodeError::ServerUnexpectedExit(format!("{}", e)))?; self.server_paths .write_pid(child.id().expect("expected server to have pid"))?; @@ -860,3 +868,13 @@ pub async fn download_cli_into_cache( } } } + +#[cfg(target_os = "windows")] +async fn get_should_use_breakaway_from_job() -> bool { + let mut cmd = Command::new("cmd"); + cmd.creation_flags( + winapi::um::winbase::CREATE_NO_WINDOW | winapi::um::winbase::CREATE_BREAKAWAY_FROM_JOB, + ); + + cmd.args(["/C", "echo ok"]).output().await.is_ok() +} diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index 9aae5ef3f07..dfb5e381179 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -10,7 +10,7 @@ use crate::options::Quality; use crate::rpc::{MaybeSync, RpcBuilder, RpcCaller, RpcDispatcher}; use crate::self_update::SelfUpdate; use crate::state::LauncherPaths; -use crate::tunnels::protocol::{HttpRequestParams, METHOD_CHALLENGE_ISSUE}; +use crate::tunnels::protocol::{HttpRequestParams, PortPrivacy, METHOD_CHALLENGE_ISSUE}; use crate::tunnels::socket_signal::CloseReason; use crate::update_service::{Platform, Release, TargetKind, UpdateService}; use crate::util::command::new_tokio_command; @@ -760,17 +760,18 @@ async fn handle_serve( macro_rules! do_setup { ($sb:expr) => { match $sb.get_running().await? { - Some(AnyCodeServer::Socket(s)) => s, + Some(AnyCodeServer::Socket(s)) => ($sb, Ok(s)), Some(_) => return Err(AnyError::from(MismatchedLaunchModeError())), None => { $sb.setup().await?; - $sb.listen_on_default_socket().await? + let r = $sb.listen_on_default_socket().await; + ($sb, r) } } }; } - let server = if params.use_local_download { + let (sb, server) = if params.use_local_download { let sb = ServerBuilder::new( &install_log, &resolved, @@ -784,6 +785,24 @@ async fn handle_serve( do_setup!(sb) }; + let server = match server { + Ok(s) => s, + Err(e) => { + // we don't loop to avoid doing so infinitely: allow the client to reconnect in this case. + if let AnyError::CodeError(CodeError::ServerUnexpectedExit(ref e)) = e { + warning!( + c.log, + "({}), removing server due to possible corruptions", + e + ); + if let Err(e) = sb.evict().await { + warning!(c.log, "Failed to evict server: {}", e); + } + } + return Err(e); + } + }; + server_ref.replace(server.clone()); server } @@ -901,9 +920,14 @@ async fn handle_update( info!(log, "Updating CLI to {}", latest_release); - updater + let r = updater .do_update(&latest_release, SilentCopyProgress()) - .await?; + .await; + + if let Err(e) = r { + did_update.store(false, Ordering::SeqCst); + return Err(e); + } Ok(UpdateResult { up_to_date: true, @@ -1077,8 +1101,16 @@ async fn handle_forward( let port_forwarding = port_forwarding .as_ref() .ok_or(CodeError::PortForwardingNotAvailable)?; - info!(log, "Forwarding port {}", params.port); - let uri = port_forwarding.forward(params.port).await?; + info!( + log, + "Forwarding port {} (public={})", params.port, params.public + ); + let privacy = match params.public { + true => PortPrivacy::Public, + false => PortPrivacy::Private, + }; + + let uri = port_forwarding.forward(params.port, privacy).await?; Ok(ForwardResult { uri }) } diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index 3bf8a331e74..a964b446384 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use super::protocol::{self, PortPrivacy}; +use super::protocol::{self, PortPrivacy, PortProtocol}; use crate::auth; use crate::constants::{IS_INTERACTIVE_CLI, PROTOCOL_VERSION_TAG, TUNNEL_SERVICE_USER_AGENT}; use crate::state::{LauncherPaths, PersistedState}; @@ -221,8 +221,11 @@ impl ActiveTunnel { &self, port_number: u16, privacy: PortPrivacy, + protocol: PortProtocol, ) -> Result<(), AnyError> { - self.manager.add_port_tcp(port_number, privacy).await?; + self.manager + .add_port_tcp(port_number, privacy, protocol) + .await?; Ok(()) } @@ -968,18 +971,18 @@ impl ActiveTunnelManager { } /// Adds a port for TCP/IP forwarding. - #[allow(dead_code)] // todo: port forwarding pub async fn add_port_tcp( &self, port_number: u16, privacy: PortPrivacy, + protocol: PortProtocol, ) -> Result<(), WrappedError> { self.relay .lock() .await .add_port(&TunnelPort { port_number, - protocol: Some(TUNNEL_PROTOCOL_AUTO.to_owned()), + protocol: Some(protocol.to_contract_str().to_string()), access_control: Some(privacy_to_tunnel_acl(privacy)), ..Default::default() }) diff --git a/cli/src/tunnels/local_forwarding.rs b/cli/src/tunnels/local_forwarding.rs index e6410860cb0..93c2d244159 100644 --- a/cli/src/tunnels/local_forwarding.rs +++ b/cli/src/tunnels/local_forwarding.rs @@ -27,7 +27,7 @@ use super::{ protocol::{ self, forward_singleton::{PortList, SetPortsResponse}, - PortPrivacy, + PortPrivacy, PortProtocol, }, shutdown_signal::ShutdownSignal, }; @@ -71,8 +71,13 @@ impl PortCount { } } } +#[derive(Clone)] +struct PortMapRec { + count: PortCount, + protocol: PortProtocol, +} -type PortMap = HashMap; +type PortMap = HashMap; /// The PortForwardingHandle is given out to multiple consumers to allow /// them to set_ports that they want to be forwarded. @@ -99,8 +104,8 @@ impl PortForwardingSender { for p in current.iter() { if !ports.contains(p) { let n = v.get_mut(&p.number).expect("expected port in map"); - n[p.privacy] -= 1; - if n.is_empty() { + n.count[p.privacy] -= 1; + if n.count.is_empty() { v.remove(&p.number); } } @@ -110,12 +115,19 @@ impl PortForwardingSender { if !current.contains(p) { match v.get_mut(&p.number) { Some(n) => { - n[p.privacy] += 1; + n.count[p.privacy] += 1; + n.protocol = p.protocol; } None => { - let mut pc = PortCount::default(); - pc[p.privacy] += 1; - v.insert(p.number, pc); + let mut count = PortCount::default(); + count[p.privacy] += 1; + v.insert( + p.number, + PortMapRec { + count, + protocol: p.protocol, + }, + ); } }; } @@ -164,22 +176,34 @@ impl PortForwardingReceiver { while self.receiver.changed().await.is_ok() { let next = self.receiver.borrow().clone(); - for (port, count) in current.iter() { - let privacy = count.primary_privacy(); - if !matches!(next.get(port), Some(n) if n.primary_privacy() == privacy) { + for (port, rec) in current.iter() { + let privacy = rec.count.primary_privacy(); + if !matches!(next.get(port), Some(n) if n.count.primary_privacy() == privacy) { match tunnel.remove_port(*port).await { - Ok(_) => info!(log, "stopped forwarding port {} at {:?}", *port, privacy), - Err(e) => error!(log, "failed to stop forwarding port {}: {}", port, e), + Ok(_) => info!( + log, + "stopped forwarding {} port {} at {:?}", rec.protocol, *port, privacy + ), + Err(e) => error!( + log, + "failed to stop forwarding {} port {}: {}", rec.protocol, port, e + ), } } } - for (port, count) in next.iter() { - let privacy = count.primary_privacy(); - if !matches!(current.get(port), Some(n) if n.primary_privacy() == privacy) { - match tunnel.add_port_tcp(*port, privacy).await { - Ok(_) => info!(log, "forwarding port {} at {:?}", port, privacy), - Err(e) => error!(log, "failed to forward port {}: {}", port, e), + for (port, rec) in next.iter() { + let privacy = rec.count.primary_privacy(); + if !matches!(current.get(port), Some(n) if n.count.primary_privacy() == privacy) { + match tunnel.add_port_tcp(*port, privacy, rec.protocol).await { + Ok(_) => info!( + log, + "forwarding {} port {} at {:?}", rec.protocol, port, privacy + ), + Err(e) => error!( + log, + "failed to forward {} port {}: {}", rec.protocol, port, e + ), } } } diff --git a/cli/src/tunnels/port_forwarder.rs b/cli/src/tunnels/port_forwarder.rs index 093c1c8176f..b05ae95ae40 100644 --- a/cli/src/tunnels/port_forwarder.rs +++ b/cli/src/tunnels/port_forwarder.rs @@ -12,10 +12,13 @@ use crate::{ util::errors::{AnyError, CannotForwardControlPort, ServerHasClosed}, }; -use super::{dev_tunnels::ActiveTunnel, protocol::PortPrivacy}; +use super::{ + dev_tunnels::ActiveTunnel, + protocol::{PortPrivacy, PortProtocol}, +}; pub enum PortForwardingRec { - Forward(u16, oneshot::Sender>), + Forward(u16, PortPrivacy, oneshot::Sender>), Unforward(u16, oneshot::Sender>), } @@ -54,8 +57,9 @@ impl PortForwardingProcessor { /// Processes the incoming forwarding request. pub async fn process(&mut self, req: PortForwardingRec, tunnel: &mut ActiveTunnel) { match req { - PortForwardingRec::Forward(port, tx) => { - tx.send(self.process_forward(port, tunnel).await).ok(); + PortForwardingRec::Forward(port, privacy, tx) => { + tx.send(self.process_forward(port, privacy, tunnel).await) + .ok(); } PortForwardingRec::Unforward(port, tx) => { tx.send(self.process_unforward(port, tunnel).await).ok(); @@ -80,6 +84,7 @@ impl PortForwardingProcessor { async fn process_forward( &mut self, port: u16, + privacy: PortPrivacy, tunnel: &mut ActiveTunnel, ) -> Result { if port == CONTROL_PORT { @@ -87,7 +92,9 @@ impl PortForwardingProcessor { } if !self.forwarded.contains(&port) { - tunnel.add_port_tcp(port, PortPrivacy::Private).await?; + tunnel + .add_port_tcp(port, privacy, PortProtocol::Auto) + .await?; self.forwarded.insert(port); } @@ -101,9 +108,9 @@ pub struct PortForwarding { } impl PortForwarding { - pub async fn forward(&self, port: u16) -> Result { + pub async fn forward(&self, port: u16, privacy: PortPrivacy) -> Result { let (tx, rx) = oneshot::channel(); - let req = PortForwardingRec::Forward(port, tx); + let req = PortForwardingRec::Forward(port, privacy, tx); if self.tx.send(req).await.is_err() { return Err(ServerHasClosed().into()); diff --git a/cli/src/tunnels/protocol.rs b/cli/src/tunnels/protocol.rs index 6434faadc39..3654826c57e 100644 --- a/cli/src/tunnels/protocol.rs +++ b/cli/src/tunnels/protocol.rs @@ -47,6 +47,8 @@ pub struct HttpHeadersParams { #[derive(Deserialize, Debug)] pub struct ForwardParams { pub port: u16, + #[serde(default)] + pub public: bool, } #[derive(Deserialize, Debug)] @@ -297,10 +299,40 @@ pub enum PortPrivacy { Private, } +#[derive(Serialize, Deserialize, PartialEq, Copy, Eq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum PortProtocol { + Auto, + Http, + Https, +} + +impl std::fmt::Display for PortProtocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_contract_str()) + } +} + +impl Default for PortProtocol { + fn default() -> Self { + Self::Auto + } +} + +impl PortProtocol { + pub fn to_contract_str(&self) -> &'static str { + match *self { + Self::Auto => tunnels::contracts::TUNNEL_PROTOCOL_AUTO, + Self::Http => tunnels::contracts::TUNNEL_PROTOCOL_HTTP, + Self::Https => tunnels::contracts::TUNNEL_PROTOCOL_HTTPS, + } + } +} + pub mod forward_singleton { use serde::{Deserialize, Serialize}; - use super::PortPrivacy; + use super::{PortPrivacy, PortProtocol}; pub const METHOD_SET_PORTS: &str = "set_ports"; @@ -308,6 +340,7 @@ pub mod forward_singleton { pub struct PortRec { pub number: u16, pub privacy: PortPrivacy, + pub protocol: PortProtocol, } pub type PortList = Vec; diff --git a/cli/src/tunnels/service_macos.rs b/cli/src/tunnels/service_macos.rs index 2d0a23f8cb2..2a51681de1d 100644 --- a/cli/src/tunnels/service_macos.rs +++ b/cli/src/tunnels/service_macos.rs @@ -86,7 +86,7 @@ impl ServiceManager for LaunchdService { match capture_command_and_check_status("launchctl", &["stop", &get_service_label()]).await { Ok(_) => {} // status 3 == "no such process" - Err(CodeError::CommandFailed { code, .. }) if code == 3 => {} + Err(CodeError::CommandFailed { code: 3, .. }) => {} Err(e) => return Err(wrap(e, "error stopping service").into()), }; diff --git a/cli/src/update_service.rs b/cli/src/update_service.rs index 4bec13d6e86..90339148188 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use std::{ffi::OsStr, fmt, path::Path}; +use std::{fmt, path::Path}; use serde::{Deserialize, Serialize}; @@ -11,10 +11,11 @@ use crate::{ constants::VSCODE_CLI_UPDATE_ENDPOINT, debug, log, options, spanf, util::{ - errors::{AnyError, CodeError, WrappedError}, + errors::{wrap, AnyError, CodeError, WrappedError}, http::{BoxedHttp, SimpleResponse}, io::ReportCopyProgress, - tar, zipper, + tar::{self, has_gzip_header}, + zipper, }, }; @@ -178,10 +179,10 @@ pub fn unzip_downloaded_release( where T: ReportCopyProgress, { - if compressed_file.extension() == Some(OsStr::new("zip")) { - zipper::unzip_file(compressed_file, target_dir, reporter) - } else { - tar::decompress_tarball(compressed_file, target_dir, reporter) + match has_gzip_header(compressed_file) { + Ok((f, true)) => tar::decompress_tarball(f, target_dir, reporter), + Ok((f, false)) => zipper::unzip_file(f, target_dir, reporter), + Err(e) => Err(wrap(e, "error checking for gzip header")), } } @@ -249,7 +250,7 @@ impl Platform { Platform::DarwinARM64 => "server-darwin-arm64", Platform::WindowsX64 => "server-win32-x64", Platform::WindowsX86 => "server-win32", - Platform::WindowsARM64 => "server-win32-x64", // we don't publish an arm64 server build yet + Platform::WindowsARM64 => "server-win32-arm64", } .to_owned() } diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index fc706199aab..67519d5437e 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -512,10 +512,16 @@ pub enum CodeError { // todo: can be specialized when update service is moved to CodeErrors #[error("Could not check for update: {0}")] UpdateCheckFailed(String), + #[error("Could not read connection token file: {0}")] + CouldNotReadConnectionTokenFile(std::io::Error), #[error("Could not write connection token file: {0}")] CouldNotCreateConnectionTokenFile(std::io::Error), #[error("A tunnel with the name {0} exists and is in-use. Please pick a different name or stop the existing tunnel.")] TunnelActiveAndInUse(String), + #[error("Timed out looking for port/socket")] + ServerOriginTimeout, + #[error("Server exited without writing port/socket: {0}")] + ServerUnexpectedExit(String), } makeAnyError!( diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index 20a5bc94b37..0f49ab20887 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -19,12 +19,22 @@ lazy_static! { static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap(); static ref LIBSTD_CXX_VERSION_RE: BinRegex = BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); - static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); - static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); 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"; pub struct PreReqChecker {} @@ -64,7 +74,8 @@ impl PreReqChecker { } else { println!("!!! WARNING: Skipping server pre-requisite check !!!"); println!("!!! Server stability is not guaranteed. Proceed at your own risk. !!!"); - (Ok(false), Ok(false)) + // Use the legacy server for #210029 + (Ok(true), Ok(true)) }; match (&gnu_a, &gnu_b, is_nixos) { diff --git a/cli/src/util/tar.rs b/cli/src/util/tar.rs index 0a5496411f7..10577140325 100644 --- a/cli/src/util/tar.rs +++ b/cli/src/util/tar.rs @@ -5,15 +5,15 @@ use crate::util::errors::{wrap, WrappedError}; use flate2::read::GzDecoder; -use std::fs; -use std::io::Seek; +use std::fs::{self, File}; +use std::io::{Read, Seek}; use std::path::{Path, PathBuf}; use tar::Archive; use super::errors::wrapdbg; use super::io::ReportCopyProgress; -fn should_skip_first_segment(file: &fs::File) -> Result { +fn should_skip_first_segment(file: &fs::File) -> Result<(bool, u64), WrappedError> { // unfortunately, we need to re-read the archive here since you cannot reuse // `.entries()`. But this will generally only look at one or two files, so this // should be acceptably speedy... If not, we could hardcode behavior for @@ -39,30 +39,35 @@ fn should_skip_first_segment(file: &fs::File) -> Result { .to_owned() }; - let mut had_multiple = false; + let mut num_entries = 1; + let mut had_different_prefixes = false; for file in entries.flatten() { - had_multiple = true; - if let Ok(name) = file.path() { - if name.iter().next() != Some(&first_name) { - return Ok(false); + if !had_different_prefixes { + if let Ok(name) = file.path() { + if name.iter().next() != Some(&first_name) { + had_different_prefixes = true; + } } } + + num_entries += 1; } - Ok(had_multiple) // prefix removal is invalid if there's only a single file + Ok((!had_different_prefixes && num_entries > 1, num_entries)) // prefix removal is invalid if there's only a single file } pub fn decompress_tarball( - path: &Path, + mut tar_gz: File, parent_path: &Path, mut reporter: T, ) -> Result<(), WrappedError> where T: ReportCopyProgress, { - let mut tar_gz = fs::File::open(path) - .map_err(|e| wrap(e, format!("error opening file {}", path.display())))?; - let skip_first = should_skip_first_segment(&tar_gz)?; + let (skip_first, num_entries) = should_skip_first_segment(&tar_gz)?; + let report_progress_every = num_entries / 20; + let mut entries_so_far = 0; + let mut last_reported_at = 0; // reset since skip logic read the tar already: tar_gz @@ -71,12 +76,19 @@ where let tar = GzDecoder::new(tar_gz); let mut archive = Archive::new(tar); - - let results = archive + archive .entries() - .map_err(|e| wrap(e, format!("error opening archive {}", path.display())))? + .map_err(|e| wrap(e, "error opening archive"))? .filter_map(|e| e.ok()) - .map(|mut entry| { + .try_for_each::<_, Result<_, WrappedError>>(|mut entry| { + // approximate progress based on where we are in the archive: + entries_so_far += 1; + if entries_so_far - last_reported_at > report_progress_every { + reporter.report_progress(entries_so_far, num_entries); + entries_so_far += 1; + last_reported_at = entries_so_far; + } + let entry_path = entry .path() .map_err(|e| wrap(e, "error reading entry path"))?; @@ -95,12 +107,21 @@ where entry .unpack(&path) .map_err(|e| wrapdbg(e, format!("error unpacking {}", path.display())))?; - Ok(path) - }) - .collect::, WrappedError>>()?; - // Tarballs don't have a way to get the number of entries ahead of time - reporter.report_progress(results.len() as u64, results.len() as u64); + Ok(()) + })?; + + reporter.report_progress(num_entries, num_entries); Ok(()) } + +pub fn has_gzip_header(path: &Path) -> std::io::Result<(File, bool)> { + let mut file = fs::File::open(path)?; + let mut header = [0; 2]; + let _ = file.read_exact(&mut header); + + file.rewind()?; + + Ok((file, header[0] == 0x1f && header[1] == 0x8b)) +} diff --git a/cli/src/util/zipper.rs b/cli/src/util/zipper.rs index 0e9939d4db5..5521fa42c54 100644 --- a/cli/src/util/zipper.rs +++ b/cli/src/util/zipper.rs @@ -44,19 +44,20 @@ fn should_skip_first_segment(archive: &mut ZipArchive) -> bool { archive.len() > 1 // prefix removal is invalid if there's only a single file } -pub fn unzip_file(path: &Path, parent_path: &Path, mut reporter: T) -> Result<(), WrappedError> +pub fn unzip_file(file: File, parent_path: &Path, mut reporter: T) -> Result<(), WrappedError> where T: ReportCopyProgress, { - let file = fs::File::open(path) - .map_err(|e| wrap(e, format!("unable to open file {}", path.display())))?; - - let mut archive = zip::ZipArchive::new(file) - .map_err(|e| wrap(e, format!("failed to open zip archive {}", path.display())))?; + let mut archive = + zip::ZipArchive::new(file).map_err(|e| wrap(e, "failed to open zip archive"))?; let skip_segments_no = usize::from(should_skip_first_segment(&mut archive)); + let report_progress_every = archive.len() / 20; + for i in 0..archive.len() { - reporter.report_progress(i as u64, archive.len() as u64); + if i % report_progress_every == 0 { + reporter.report_progress(i as u64, archive.len() as u64); + } let mut file = archive .by_index(i) .map_err(|e| wrap(e, format!("could not open zip entry {}", i)))?; diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index b80a187e266..f6bfd751895 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -11,7 +11,9 @@ "icon": "images/icon.png", "activationEvents": [ "onProfile", - "onProfile:github" + "onProfile:github", + "onLanguage:json", + "onLanguage:jsonc" ], "enabledApiProposals": [ "profileContentHandlers" @@ -161,7 +163,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json b/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json index 681ca6105cf..3f8400a7bd4 100644 --- a/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.codespaces.schema.json @@ -180,6 +180,11 @@ "items": { "type": "string" } + }, + "disableAutomaticConfiguration": { + "type": "boolean", + "description": "Disables the setup that is automatically run in a codespace if no `postCreateCommand` is specified.", + "default": false } } } diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 7672e88e7a4..0e7d733c76f 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -125,10 +125,12 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" before-after-hook@^2.2.0: version "2.2.3" @@ -174,6 +176,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index 3a5459401f9..0bf8df9dc01 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -14,7 +14,8 @@ { "open": "(", "close": ")" }, { "open": "'", "close": "'", "notIn": ["string", "comment"] }, { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "/*", "close": "*/", "notIn": ["string", "comment"] } + { "open": "/*", "close": "*/", "notIn": ["string", "comment"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], diff --git a/extensions/cpp/package.json b/extensions/cpp/package.json index c1d3f4882f6..9f3c890a48b 100644 --- a/extensions/cpp/package.json +++ b/extensions/cpp/package.json @@ -30,9 +30,13 @@ "id": "cpp", "extensions": [ ".cpp", + ".cppm", ".cc", + ".ccm", ".cxx", + ".cxxm", ".c++", + ".c++m", ".hpp", ".hh", ".hxx", diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index 1c88bd17296..58a7408dbbe 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": "7a7482ffc72a6677a87eb1ed76005593a4f7f131" + "commitHash": "d63e2661d4e0c83b6c7810eb1d0eedc5da843b04" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index 96dbe04473c..4a2497a064a 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/7a7482ffc72a6677a87eb1ed76005593a4f7f131", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/d63e2661d4e0c83b6c7810eb1d0eedc5da843b04", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -512,6 +512,9 @@ { "include": "#type-name" }, + { + "include": "#type-arguments" + }, { "include": "#attribute-arguments" } diff --git a/extensions/css-language-features/client/src/browser/cssClientMain.ts b/extensions/css-language-features/client/src/browser/cssClientMain.ts index 6522c786389..1d4153d9836 100644 --- a/extensions/css-language-features/client/src/browser/cssClientMain.ts +++ b/extensions/css-language-features/client/src/browser/cssClientMain.ts @@ -7,13 +7,7 @@ import { ExtensionContext, Uri, l10n } from 'vscode'; import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../cssClient'; import { LanguageClient } from 'vscode-languageclient/browser'; - -declare const Worker: { - new(stringUrl: string): any; -}; -declare const TextDecoder: { - new(encoding?: string): { decode(buffer: ArrayBuffer): string }; -}; +import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource'; let client: BaseLanguageClient | undefined; @@ -25,11 +19,12 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; client = await startClient(context, newLanguageClient, { TextDecoder }); + context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' })); } catch (e) { console.log(e); } diff --git a/extensions/css-language-features/client/src/dropOrPaste/dropOrPasteResource.ts b/extensions/css-language-features/client/src/dropOrPaste/dropOrPasteResource.ts new file mode 100644 index 00000000000..6a4c38d2417 --- /dev/null +++ b/extensions/css-language-features/client/src/dropOrPaste/dropOrPasteResource.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 * as path from 'path'; +import * as vscode from 'vscode'; +import { getDocumentDir, Mimes, Schemes } from './shared'; +import { UriList } from './uriList'; + +class DropOrPasteResourceProvider implements vscode.DocumentDropEditProvider, vscode.DocumentPasteEditProvider { + readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('css', 'url'); + + async provideDocumentDropEdits( + document: vscode.TextDocument, + position: vscode.Position, + dataTransfer: vscode.DataTransfer, + token: vscode.CancellationToken, + ): Promise { + const uriList = await this.getUriList(dataTransfer); + if (!uriList.entries.length || token.isCancellationRequested) { + return; + } + + const snippet = await this.createUriListSnippet(document.uri, uriList); + if (!snippet || token.isCancellationRequested) { + return; + } + + return { + kind: this.kind, + title: snippet.label, + insertText: snippet.snippet.value, + yieldTo: this.pasteAsCssUrlByDefault(document, position) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')] + }; + } + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + ranges: readonly vscode.Range[], + dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, + token: vscode.CancellationToken + ): Promise { + const uriList = await this.getUriList(dataTransfer); + if (!uriList.entries.length || token.isCancellationRequested) { + return; + } + + const snippet = await this.createUriListSnippet(document.uri, uriList); + if (!snippet || token.isCancellationRequested) { + return; + } + + return [{ + kind: this.kind, + title: snippet.label, + insertText: snippet.snippet.value, + yieldTo: this.pasteAsCssUrlByDefault(document, ranges[0].start) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')] + }]; + } + + private async getUriList(dataTransfer: vscode.DataTransfer): Promise { + const urlList = await dataTransfer.get(Mimes.uriList)?.asString(); + if (urlList) { + return UriList.from(urlList); + } + + // Find file entries + const uris: vscode.Uri[] = []; + for (const [_, entry] of dataTransfer) { + const file = entry.asFile(); + if (file?.uri) { + uris.push(file.uri); + } + } + + return new UriList(uris.map(uri => ({ uri, str: uri.toString(true) }))); + } + + private async createUriListSnippet(docUri: vscode.Uri, uriList: UriList): Promise<{ readonly snippet: vscode.SnippetString; readonly label: string } | undefined> { + if (!uriList.entries.length) { + return; + } + + const snippet = new vscode.SnippetString(); + for (let i = 0; i < uriList.entries.length; i++) { + const uri = uriList.entries[i]; + const relativePath = getRelativePath(getDocumentDir(docUri), uri.uri); + const urlText = relativePath ?? uri.str; + + snippet.appendText(`url(${urlText})`); + if (i !== uriList.entries.length - 1) { + snippet.appendText(' '); + } + } + + return { + snippet, + label: uriList.entries.length > 1 + ? vscode.l10n.t('Insert url() Functions') + : vscode.l10n.t('Insert url() Function') + }; + } + + private pasteAsCssUrlByDefault(document: vscode.TextDocument, position: vscode.Position): boolean { + const regex = /url\(.+?\)/gi; + for (const match of Array.from(document.lineAt(position.line).text.matchAll(regex))) { + if (position.character > match.index && position.character < match.index + match[0].length) { + return false; + } + } + return true; + } +} + +function getRelativePath(fromFile: vscode.Uri | undefined, toFile: vscode.Uri): string | undefined { + if (fromFile && fromFile.scheme === toFile.scheme && fromFile.authority === toFile.authority) { + if (toFile.scheme === Schemes.file) { + // On windows, we must use the native `path.relative` to generate the relative path + // so that drive-letters are resolved cast insensitively. However we then want to + // convert back to a posix path to insert in to the document + const relativePath = path.relative(fromFile.fsPath, toFile.fsPath); + return path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep)); + } + + return path.posix.relative(fromFile.path, toFile.path); + } + + return undefined; +} + +export function registerDropOrPasteResourceSupport(selector: vscode.DocumentSelector): vscode.Disposable { + const provider = new DropOrPasteResourceProvider(); + + return vscode.Disposable.from( + vscode.languages.registerDocumentDropEditProvider(selector, provider, { + providedDropEditKinds: [provider.kind], + dropMimeTypes: [ + Mimes.uriList, + 'files' + ] + }), + vscode.languages.registerDocumentPasteEditProvider(selector, provider, { + providedPasteEditKinds: [provider.kind], + pasteMimeTypes: [ + Mimes.uriList, + 'files' + ] + }) + ); +} diff --git a/extensions/css-language-features/client/src/dropOrPaste/shared.ts b/extensions/css-language-features/client/src/dropOrPaste/shared.ts new file mode 100644 index 00000000000..548bccfec69 --- /dev/null +++ b/extensions/css-language-features/client/src/dropOrPaste/shared.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 * as vscode from 'vscode'; +import { Utils } from 'vscode-uri'; + +export const Schemes = Object.freeze({ + file: 'file', + notebookCell: 'vscode-notebook-cell', + untitled: 'untitled', +}); + +export const Mimes = Object.freeze({ + plain: 'text/plain', + uriList: 'text/uri-list', +}); + + +export function getDocumentDir(uri: vscode.Uri): vscode.Uri | undefined { + const docUri = getParentDocumentUri(uri); + if (docUri.scheme === Schemes.untitled) { + return vscode.workspace.workspaceFolders?.[0]?.uri; + } + return Utils.dirname(docUri); +} + +function getParentDocumentUri(uri: vscode.Uri): vscode.Uri { + if (uri.scheme === Schemes.notebookCell) { + // is notebook documents necessary? + for (const notebook of vscode.workspace.notebookDocuments) { + for (const cell of notebook.getCells()) { + if (cell.document.uri.toString() === uri.toString()) { + return notebook.uri; + } + } + } + } + + return uri; +} diff --git a/extensions/css-language-features/client/src/dropOrPaste/uriList.ts b/extensions/css-language-features/client/src/dropOrPaste/uriList.ts new file mode 100644 index 00000000000..ed20b1ee797 --- /dev/null +++ b/extensions/css-language-features/client/src/dropOrPaste/uriList.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +function splitUriList(str: string): string[] { + return str.split('\r\n'); +} + +function parseUriList(str: string): string[] { + return splitUriList(str) + .filter(value => !value.startsWith('#')) // Remove comments + .map(value => value.trim()); +} + +export class UriList { + + static from(str: string): UriList { + return new UriList(coalesce(parseUriList(str).map(line => { + try { + return { uri: vscode.Uri.parse(line), str: line }; + } catch { + // Uri parse failure + return undefined; + } + }))); + } + + constructor( + public readonly entries: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str: string }> + ) { } +} + +function coalesce(array: ReadonlyArray): T[] { + return array.filter(e => !!e); +} diff --git a/extensions/css-language-features/client/src/node/cssClientMain.ts b/extensions/css-language-features/client/src/node/cssClientMain.ts index 802b58ab286..96926979b2a 100644 --- a/extensions/css-language-features/client/src/node/cssClientMain.ts +++ b/extensions/css-language-features/client/src/node/cssClientMain.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getNodeFSRequestService } from './nodeFs'; -import { ExtensionContext, extensions, l10n } from 'vscode'; -import { startClient, LanguageClientConstructor } from '../cssClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; import { TextDecoder } from 'util'; - +import { ExtensionContext, extensions, l10n } from 'vscode'; +import { BaseLanguageClient, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node'; +import { LanguageClientConstructor, startClient } from '../cssClient'; +import { getNodeFSRequestService } from './nodeFs'; +import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource'; let client: BaseLanguageClient | undefined; @@ -37,6 +37,8 @@ export async function activate(context: ExtensionContext) { process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; client = await startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder }); + + context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' })); } export async function deactivate(): Promise { @@ -45,3 +47,4 @@ export async function deactivate(): Promise { client = undefined; } } + diff --git a/extensions/css-language-features/client/tsconfig.json b/extensions/css-language-features/client/tsconfig.json index 573b24b4aa6..17bf7e962a8 100644 --- a/extensions/css-language-features/client/tsconfig.json +++ b/extensions/css-language-features/client/tsconfig.json @@ -1,10 +1,14 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", - "../../../src/vscode-dts/vscode.d.ts" + "../../../src/vscode-dts/vscode.d.ts", + "../../../src/vscode-dts/vscode.proposed.documentPaste.d.ts" ] } diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index e105c0090ee..57cbba323a4 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -23,6 +23,9 @@ "supported": true } }, + "enabledApiProposals": [ + "documentPaste" + ], "scripts": { "compile": "npx gulp compile-extension:css-language-features-client compile-extension:css-language-features-server", "watch": "npx gulp watch-extension:css-language-features-client watch-extension:css-language-features-server", @@ -994,11 +997,11 @@ ] }, "dependencies": { - "vscode-languageclient": "^9.0.1", + "vscode-languageclient": "^10.0.0-next.8", "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/css-language-features/schemas/package.schema.json b/extensions/css-language-features/schemas/package.schema.json index 831149caa9e..6c4b91faa27 100644 --- a/extensions/css-language-features/schemas/package.schema.json +++ b/extensions/css-language-features/schemas/package.schema.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CSS contributions to package.json", "type": "object", "properties": { "contributes": { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 0a790771db2..fe4f64d7c01 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -11,13 +11,13 @@ "browser": "./dist/browser/cssServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.13", - "vscode-languageserver": "^10.0.0-next.2", + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index b7f86c89a11..59033f770c1 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.19.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.8.tgz#c1e42b165e5a526caf1f010747e0522cb2c9c36a" - integrity sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== dependencies: undici-types "~5.26.4" @@ -24,28 +24,28 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-css-languageservice@^6.2.13: - version "6.2.13" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz#c7c2dc7a081a203048d60157c65536767d6d96f8" - integrity sha512-2rKWXfH++Kxd9Z4QuEgd1IF7WmblWWU7DScuyf1YumoGLkY9DW6wF/OTlhOyO2rN63sWHX2dehIpKBbho4ZwvA== +vscode-css-languageservice@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.0.tgz#51724d193d19b1a9075b1cef5cfeea6a555d2aa4" + integrity sha512-nU92imtkgzpCL0xikrIb8WvedV553F2BENzgz23wFuok/HLN5BeQmroMy26pUwFxV2eV8oNRmYCUv8iO7kSMhw== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.3.tgz#09d3e28e9ad12270233d07fa0b69cf1d51d7dfe4" - integrity sha512-H8ATH5SAvc3JzttS+AL6g681PiBOZM/l34WP2JZk4akY3y7NqTP+f9cJ+MhrVBbD3aDS8bdAKewZgbFLW6M8Pg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -57,17 +57,17 @@ vscode-languageserver-types@3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== -vscode-languageserver@^10.0.0-next.2: - version "10.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.2.tgz#9a8ac58f72979961497c4fd7f6097561d4134d5f" - integrity sha512-WZdK/XO6EkNU6foYck49NpS35sahWhYFs4hwCGalH/6lhPmdUKABTnWioK/RLZKWqH8E5HdlAHQMfSBIxKBV9Q== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.3" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index d01914101ca..eef1c9ab57d 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" balanced-match@^1.0.0: version "1.0.2" @@ -26,46 +28,51 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -minimatch@^5.1.0: - version "5.1.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== +minimatch@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" -semver@^7.3.7: +semver@^7.6.0: version "7.6.0" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" -vscode-jsonrpc@8.2.0: - version "8.2.0" - resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz" - integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-languageclient@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz" - integrity sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== + +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: - minimatch "^5.1.0" - semver "^7.3.7" - vscode-languageserver-protocol "3.17.5" + minimatch "^9.0.3" + semver "^7.6.0" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.5: - version "3.17.5" - resolved "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz" - integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "8.2.0" - vscode-languageserver-types "3.17.5" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.5: - version "3.17.5" - resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz" - integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/dart/cgmanifest.json b/extensions/dart/cgmanifest.json index 9c90588adf1..df4e4f0aae9 100644 --- a/extensions/dart/cgmanifest.json +++ b/extensions/dart/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dart-lang/dart-syntax-highlight", "repositoryUrl": "https://github.com/dart-lang/dart-syntax-highlight", - "commitHash": "272e2f89f85073c04b7e15b582257f76d2489970" + "commitHash": "bb8f7eebf5a1028e70dbebcf35cfef738dddc7fe" } }, "licenseDetail": [ diff --git a/extensions/dart/syntaxes/dart.tmLanguage.json b/extensions/dart/syntaxes/dart.tmLanguage.json index cc9dee8d275..5a4a9393bc7 100644 --- a/extensions/dart/syntaxes/dart.tmLanguage.json +++ b/extensions/dart/syntaxes/dart.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/dart-lang/dart-syntax-highlight/commit/272e2f89f85073c04b7e15b582257f76d2489970", + "version": "https://github.com/dart-lang/dart-syntax-highlight/commit/bb8f7eebf5a1028e70dbebcf35cfef738dddc7fe", "name": "Dart", "scopeName": "source.dart", "patterns": [ @@ -14,7 +14,7 @@ }, { "name": "meta.declaration.dart", - "begin": "^\\w*\\b(library|import|part of|part|export)\\b", + "begin": "^\\w*\\b(augment\\s+library|library|import\\s+augment|import|part\\s+of|part|export)\\b", "beginCaptures": { "0": { "name": "keyword.other.import.dart" @@ -208,7 +208,7 @@ }, { "name": "variable.language.dart", - "match": "(?!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/g; +// From src/vs/base/common/strings.ts +const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; /** * Froms vs/base/common/strings.ts in core diff --git a/extensions/debug-server-ready/yarn.lock b/extensions/debug-server-ready/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/debug-server-ready/yarn.lock +++ b/extensions/debug-server-ready/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index b7291b9552f..1783bc2ceaf 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -479,7 +479,7 @@ "deps": "yarn add @vscode/emmet-helper" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "dependencies": { "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", diff --git a/extensions/emmet/src/defaultCompletionProvider.ts b/extensions/emmet/src/defaultCompletionProvider.ts index 60cdb2a5cb1..0876cfa6f6a 100644 --- a/extensions/emmet/src/defaultCompletionProvider.ts +++ b/extensions/emmet/src/defaultCompletionProvider.ts @@ -187,13 +187,6 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi const config = getEmmetConfiguration(syntax!); const result = helper.doComplete(toLSTextDocument(document), position, syntax, config); - // https://github.com/microsoft/vscode/issues/86941 - if (result && result.items && result.items.length === 1) { - if (result.items[0].label === 'widows: ;') { - return undefined; - } - } - const newItems: vscode.CompletionItem[] = []; if (result && result.items) { result.items.forEach((item: any) => { diff --git a/extensions/emmet/src/test/completion.test.ts b/extensions/emmet/src/test/completion.test.ts index ec94a0f4928..97ac6ecff33 100644 --- a/extensions/emmet/src/test/completion.test.ts +++ b/extensions/emmet/src/test/completion.test.ts @@ -29,8 +29,30 @@ suite('Tests for completion in CSS embedded in HTML', () => { }); // https://github.com/microsoft/vscode/issues/86941 - test('#86941, widows should not be completed', async () => { - await testCompletionProvider('css', `.foo { wi| }`, undefined); + test('#86941, widows should be completed after width', async () => { + await testCompletionProvider('css', `.foo { wi| }`, [ + { label: 'width: ;', documentation: `width: |;` } + ]); + await testCompletionProvider('css', `.foo { wid| }`, [ + { label: 'width: ;', documentation: `width: |;` } + ]); + try { + await testCompletionProvider('css', `.foo { wi| }`, [ + { label: 'widows: ;', documentation: `widows: |;` } + ]); + } catch (e) { + assert.strictEqual(e.message, "Didn't find completion item with label widows: ;"); + } + try { + await testCompletionProvider('css', `.foo { wid| }`, [ + { label: 'widows: ;', documentation: `widows: |;` } + ]); + } catch (e) { + assert.strictEqual(e.message, "Didn't find completion item with label widows: ;"); + } + await testCompletionProvider('css', `.foo { wido| }`, [ + { label: 'widows: ;', documentation: `widows: |;` } + ]); }); // https://github.com/microsoft/vscode/issues/117020 diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index f177cdb5ec8..b75842fe4a4 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -53,15 +53,17 @@ resolved "https://registry.yarnpkg.com/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz#46cffea119a0a003312a21c2d9b5628cb5fcd442" integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/emmet-helper@^2.8.8": - version "2.9.2" - resolved "https://registry.yarnpkg.com/@vscode/emmet-helper/-/emmet-helper-2.9.2.tgz#cd5d1e64e7138ad76300e8cba5fd84f1c03e13ee" - integrity sha512-MaGuyW+fa13q3aYsluKqclmh62Hgp0BpKIqS66fCxfOaBcVQ1OnMQxRRgQUYnCkxFISAQlkJ0qWWPyXjro1Qrg== + version "2.9.3" + resolved "https://registry.yarnpkg.com/@vscode/emmet-helper/-/emmet-helper-2.9.3.tgz#8a8b228981fcf2d9346fdca77b9ad5a31dc09dba" + integrity sha512-rB39LHWWPQYYlYfpv9qCoZOVioPCftKXXqrsyqN1mTWZM6dTnONT63Db+03vgrBbHzJN45IrgS/AGxw9iiqfEw== dependencies: emmet "^2.4.3" jsonc-parser "^2.3.0" @@ -70,9 +72,9 @@ vscode-uri "^2.1.2" emmet@^2.4.3: - version "2.4.4" - resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.4.4.tgz#801aad64659dc76f3003130db767d77a78ac298e" - integrity sha512-v8Mwpjym55CS3EjJgiCLWUB3J2HSR93jhzXW325720u8KvYxdI2voYLstW3pHBxFz54H6jFjayR9G4LfTG0q+g== + version "2.4.7" + resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.4.7.tgz#19893c34e6274af14ea3c5729101e3c4ed18f01e" + integrity sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA== dependencies: "@emmetio/abbreviation" "^2.3.3" "@emmetio/css-abbreviation" "^2.1.8" @@ -101,6 +103,11 @@ queue@6.0.2: dependencies: inherits "~2.0.3" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-languageserver-textdocument@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.3.tgz#879f2649bfa5a6e07bc8b392c23ede2dfbf43eff" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index f45105b99d4..184d28e8df0 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -67,7 +67,7 @@ }, "devDependencies": { "@types/markdown-it": "0.0.2", - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 8bb2a4640df..b69dac0e2dd 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -66,7 +66,7 @@ export class ExtensionLinter { private folderToPackageJsonInfo: Record = {}; private packageJsonQ = new Set(); private readmeQ = new Set(); - private timer: NodeJS.Timer | undefined; + private timer: NodeJS.Timeout | undefined; private markdownIt: MarkdownItType.MarkdownIt | undefined; private parse5: typeof import('parse5') | undefined; @@ -149,7 +149,8 @@ export class ExtensionLinter { const effectiveProposalNames = extensionEnabledApiProposals[extensionId]; if (Array.isArray(effectiveProposalNames) && enabledApiProposals.children) { for (const child of enabledApiProposals.children) { - if (child.type === 'string' && !effectiveProposalNames.includes(getNodeValue(child))) { + const proposalName = child.type === 'string' ? getNodeValue(child) : undefined; + if (typeof proposalName === 'string' && !effectiveProposalNames.includes(proposalName.split('@')[0])) { const start = document.positionAt(child.offset); const end = document.positionAt(child.offset + child.length); diagnostics.push(new Diagnostic(new Range(start, end), apiProposalNotListed, DiagnosticSeverity.Error)); diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 5456b3ec040..00fad585fd1 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/node@^6.0.46": version "6.0.78" @@ -66,3 +68,8 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/fsharp/cgmanifest.json b/extensions/fsharp/cgmanifest.json index 524b3fa0d46..f10f9ca661a 100644 --- a/extensions/fsharp/cgmanifest.json +++ b/extensions/fsharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "ionide/ionide-fsgrammar", "repositoryUrl": "https://github.com/ionide/ionide-fsgrammar", - "commitHash": "7d029a46f17637228b2ee85dd02e511c3e8039b3" + "commitHash": "7d1b695da917dc4c7a0f7fb4683f42da208f87a2" } }, "license": "MIT", diff --git a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json index 5063f1c5210..41436335cdc 100644 --- a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json +++ b/extensions/fsharp/syntaxes/fsharp.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/ionide/ionide-fsgrammar/commit/7d029a46f17637228b2ee85dd02e511c3e8039b3", + "version": "https://github.com/ionide/ionide-fsgrammar/commit/7d1b695da917dc4c7a0f7fb4683f42da208f87a2", "name": "fsharp", "scopeName": "source.fsharp", "patterns": [ @@ -617,7 +617,7 @@ }, { "name": "constant.numeric.float.fsharp", - "match": "\\b-?[0-9][0-9_]*((\\.([0-9][0-9_]*([eE][+-]??[0-9][0-9_]*)?)?)|([eE][+-]??[0-9][0-9_]*))" + "match": "\\b-?[0-9][0-9_]*((\\.(?!\\.)([0-9][0-9_]*([eE][+-]??[0-9][0-9_]*)?)?)|([eE][+-]??[0-9][0-9_]*))" }, { "name": "constant.numeric.integer.nativeint.fsharp", @@ -635,7 +635,7 @@ }, "abstract_definition": { "name": "abstract.definition.fsharp", - "begin": "\\b(abstract)\\s+(member)?(\\s+\\[\\<.*\\>\\])?\\s*([_[:alpha:]0-9,\\._`\\s]+)(<)?", + "begin": "\\b(static)?\\s+(abstract)\\s+(member)?(\\s+\\[\\<.*\\>\\])?\\s*([_[:alpha:]0-9,\\._`\\s]+)(<)?", "end": "\\s*(with)\\b|=|$", "beginCaptures": { "1": { @@ -645,6 +645,9 @@ "name": "keyword.fsharp" }, "3": { + "name": "keyword.fsharp" + }, + "4": { "name": "support.function.attribute.fsharp" }, "5": { @@ -838,7 +841,7 @@ "name": "keyword.symbol.fsharp" } }, - "end": "(\\)\\s*(([?[:alpha:]0-9'`^._ ]+))+)", + "end": "(\\)\\s*(([?[:alpha:]0-9'`^._ ]+))*)", "endCaptures": { "1": { "name": "keyword.symbol.fsharp" @@ -933,7 +936,7 @@ "patterns": [ { "name": "binding.fsharp", - "begin": "\\b(let mutable|static let mutable|static let|let inline|let|and|member val|static member inline|static member|default|member|override|let!)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9\\._`\\s]+|(?<=,)\\s)*)?", + "begin": "\\b(let mutable|static let mutable|static let|let inline|let|and|member val|member inline|static member inline|static member|default|member|override|let!)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9\\._`\\s]+|(?<=,)\\s)*)?", "end": "\\s*((with\\b)|(=|\\n+=|(?<=\\=)))", "beginCaptures": { "1": { @@ -1008,7 +1011,7 @@ }, { "name": "binding.fsharp", - "begin": "\\b(static val mutable|val mutable|val)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9,\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9,\\._`\\s]+|(?<=,)\\s)*)?", + "begin": "\\b(static val mutable|val mutable|val inline|val)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9,\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9,\\._`\\s]+|(?<=,)\\s)*)?", "end": "\\n$", "beginCaptures": { "1": { diff --git a/extensions/git-base/package.json b/extensions/git-base/package.json index c65a8f47e61..3c9b07a13e8 100644 --- a/extensions/git-base/package.json +++ b/extensions/git-base/package.json @@ -104,7 +104,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/git-base/yarn.lock b/extensions/git-base/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/git-base/yarn.lock +++ b/extensions/git-base/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/git/package.json b/extensions/git/package.json index fc6ed3a1627..a7c8e455c11 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -16,6 +16,7 @@ "contribMergeEditorMenus", "contribMultiDiffEditorMenus", "contribDiffEditorGutterToolBarMenus", + "contribSourceControlHistoryItemChangesMenu", "contribSourceControlHistoryItemGroupMenu", "contribSourceControlHistoryItemMenu", "contribSourceControlInputBoxMenu", @@ -24,6 +25,7 @@ "diffCommand", "editSessionIdentityProvider", "quickDiffProvider", + "quickInputButtonLocation", "quickPickSortByLabel", "scmActionButton", "scmHistoryProvider", @@ -31,6 +33,7 @@ "scmSelectedProvider", "scmTextDocument", "scmValidation", + "tabInputMultiDiff", "tabInputTextMerge", "timeline" ], @@ -856,6 +859,27 @@ "title": "%command.openRepositoriesInParentFolders%", "category": "Git" }, + { + "command": "git.viewChanges", + "title": "%command.viewChanges%", + "icon": "$(diff-multiple)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.viewStagedChanges", + "title": "%command.viewStagedChanges%", + "icon": "$(diff-multiple)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.viewUntrackedChanges", + "title": "%command.viewUntrackedChanges%", + "icon": "$(diff-multiple)", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.viewCommit", "title": "%command.viewCommit%", @@ -869,6 +893,16 @@ "icon": "$(diff-multiple)", "category": "Git", "enablement": "!operationInProgress" + }, + { + "command": "git.copyCommitId", + "title": "%command.timelineCopyCommitId%", + "category": "Git" + }, + { + "command": "git.copyCommitMessage", + "title": "%command.timelineCopyCommitMessage%", + "category": "Git" } ], "continueEditSession": [ @@ -1365,6 +1399,18 @@ "command": "git.stashView", "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled" }, + { + "command": "git.viewChanges", + "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled" + }, + { + "command": "git.viewStagedChanges", + "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled" + }, + { + "command": "git.viewUntrackedChanges", + "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled && config.git.untrackedChanges == separate" + }, { "command": "git.viewCommit", "when": "false" @@ -1392,6 +1438,14 @@ { "command": "git.pushRef", "when": "false" + }, + { + "command": "git.copyCommitId", + "when": "false" + }, + { + "command": "git.copyCommitMessage", + "when": "false" } ], "scm/title": [ @@ -1511,6 +1565,16 @@ "when": "scmProvider == git && scmResourceGroup == index", "group": "inline@2" }, + { + "command": "git.viewStagedChanges", + "when": "scmProvider == git && scmResourceGroup == index && config.multiDiffEditor.experimental.enabled", + "group": "inline@1" + }, + { + "command": "git.viewChanges", + "when": "scmProvider == git && scmResourceGroup == workingTree && config.multiDiffEditor.experimental.enabled", + "group": "inline@1" + }, { "command": "git.cleanAll", "when": "scmProvider == git && scmResourceGroup == workingTree && config.git.untrackedChanges == mixed", @@ -1561,6 +1625,11 @@ "when": "scmProvider == git && scmResourceGroup == untracked", "group": "1_modification" }, + { + "command": "git.viewUntrackedChanges", + "when": "scmProvider == git && scmResourceGroup == untracked && config.multiDiffEditor.experimental.enabled", + "group": "inline@1" + }, { "command": "git.cleanAllUntracked", "when": "scmProvider == git && scmResourceGroup == untracked", @@ -1866,6 +1935,40 @@ "group": "1_modification@3" } ], + "scm/historyItemChanges/title": [ + { + "command": "git.fetchRef", + "group": "navigation@1", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" + }, + { + "command": "git.pullRef", + "group": "navigation@2", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" + }, + { + "command": "git.pushRef", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote", + "group": "navigation@3" + }, + { + "command": "git.publish", + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote", + "group": "navigation@3" + } + ], + "scm/historyItem/context": [ + { + "command": "git.copyCommitId", + "when": "scmProvider == git && !listMultiSelection", + "group": "1_copy@1" + }, + { + "command": "git.copyCommitMessage", + "when": "scmProvider == git && !listMultiSelection", + "group": "1_copy@2" + } + ], "scm/incomingChanges": [ { "command": "git.fetchRef", @@ -1918,23 +2021,23 @@ { "command": "git.pushRef", "group": "navigation", - "when": "scmProvider == git && scmHistoryItemGroupHasUpstream" + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" }, { "command": "git.publish", "group": "navigation", - "when": "scmProvider == git && !scmHistoryItemGroupHasUpstream" + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote" } ], "scm/outgoingChanges/context": [ { "command": "git.pushRef", - "when": "scmProvider == git && scmHistoryItemGroupHasUpstream", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote", "group": "1_modification@1" }, { "command": "git.publish", - "when": "scmProvider == git && !scmHistoryItemGroupHasUpstream", + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote", "group": "1_modification@1" } ], @@ -3352,7 +3455,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", - "jschardet": "3.0.0", + "jschardet": "3.1.3", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" @@ -3360,7 +3463,7 @@ "devDependencies": { "@types/byline": "4.2.31", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/picomatch": "2.3.0", "@types/which": "3.0.0" }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 3d0411c53dc..c2f7c3d6cfb 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -118,6 +118,9 @@ "command.timelineCompareWithSelected": "Compare with Selected", "command.manageUnsafeRepositories": "Manage Unsafe Repositories", "command.openRepositoriesInParentFolders": "Open Repositories In Parent Folders", + "command.viewChanges": "View Changes", + "command.viewStagedChanges": "View Staged Changes", + "command.viewUntrackedChanges": "View Untracked Changes", "command.viewAllChanges": "View All Changes", "command.viewCommit": "View Commit", "command.api.getRepositories": "Get Repositories", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 38aa9ec4236..332c328b2d1 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -44,6 +44,7 @@ export class ApiRepositoryState implements RepositoryState { get mergeChanges(): Change[] { return this._repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); } get indexChanges(): Change[] { return this._repository.indexGroup.resourceStates.map(r => new ApiChange(r)); } get workingTreeChanges(): Change[] { return this._repository.workingTreeGroup.resourceStates.map(r => new ApiChange(r)); } + get untrackedChanges(): Change[] { return this._repository.untrackedGroup.resourceStates.map(r => new ApiChange(r)); } readonly onDidChange: Event = this._repository.onDidRunGitStatus; @@ -160,10 +161,6 @@ export class ApiRepository implements Repository { return this.repository.diffBetween(ref1, ref2, path); } - getDiff(): Promise { - return this.repository.getDiff(); - } - hashObject(data: string): Promise { return this.repository.hashObject(data); } @@ -196,6 +193,10 @@ export class ApiRepository implements Repository { return this.repository.getRefs(query, cancellationToken); } + checkIgnore(paths: string[]): Promise> { + return this.repository.checkIgnore(paths); + } + getMergeBase(ref1: string, ref2: string): Promise { return this.repository.getMergeBase(ref1, ref2); } @@ -321,6 +322,10 @@ export class ApiImpl implements API { } async openRepository(root: Uri): Promise { + if (root.scheme !== 'file') { + return null; + } + await this._model.openRepository(root.fsPath); return this.getRepository(root) || null; } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index cf462435a2c..2abbeaaa14e 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -122,6 +122,7 @@ export interface RepositoryState { readonly mergeChanges: Change[]; readonly indexChanges: Change[]; readonly workingTreeChanges: Change[]; + readonly untrackedChanges: Change[]; readonly onDidChange: Event; } @@ -144,6 +145,8 @@ export interface LogOptions { readonly sortByAuthorDate?: boolean; readonly shortStats?: boolean; readonly author?: string; + readonly refNames?: string[]; + readonly maxParents?: number; } export interface CommitOptions { @@ -226,8 +229,6 @@ export interface Repository { diffBetween(ref1: string, ref2: string): Promise; diffBetween(ref1: string, ref2: string, path: string): Promise; - getDiff(): Promise; - hashObject(data: string): Promise; createBranch(name: string, checkout: boolean, ref?: string): Promise; @@ -237,6 +238,8 @@ export interface Repository { getBranchBase(name: string): Promise; setBranchUpstream(name: string, upstream: string): Promise; + checkIgnore(paths: string[]): Promise>; + getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise; getMergeBase(ref1: string, ref2: string): Promise; diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 22fa8212d22..cb13cca654e 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,13 +5,13 @@ 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 } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation } 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 { Repository, Resource, ResourceGroupType } from './repository'; +import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; import { dispose, grep, isDefined, isDescendant, pathEquals, relativePath } from './util'; @@ -711,10 +711,13 @@ export class CommandCenter { } try { - const [head, rebaseOrMergeHead] = await Promise.all([ + const [head, rebaseOrMergeHead, diffBetween] = await Promise.all([ repo.getCommit('HEAD'), - isRebasing ? repo.getCommit('REBASE_HEAD') : repo.getCommit('MERGE_HEAD') + isRebasing ? repo.getCommit('REBASE_HEAD') : repo.getCommit('MERGE_HEAD'), + await repo.diffBetween(isRebasing ? 'REBASE_HEAD' : 'MERGE_HEAD', 'HEAD') ]); + const diffFile = diffBetween?.find(diff => diff.uri.fsPath === uri.fsPath); + // ours (current branch and commit) current.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', '); current.description = '$(git-commit) ' + head.hash.substring(0, 7); @@ -723,7 +726,11 @@ export class CommandCenter { // theirs incoming.detail = rebaseOrMergeHead.refNames.join(', '); incoming.description = '$(git-commit) ' + rebaseOrMergeHead.hash.substring(0, 7); - incoming.uri = toGitUri(uri, rebaseOrMergeHead.hash); + if (diffFile) { + incoming.uri = toGitUri(diffFile.originalUri, rebaseOrMergeHead.hash); + } else { + incoming.uri = toGitUri(uri, rebaseOrMergeHead.hash); + } } catch (error) { // not so bad, can continue with just uris @@ -1332,14 +1339,14 @@ export class CommandCenter { @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { - this.logger.debug(`git.stage ${resourceStates.length} `); + this.logger.debug(`[CommandCenter][stage] git.stage ${resourceStates.length} `); resourceStates = resourceStates.filter(s => !!s); if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) { const resource = this.getSCMResource(); - this.logger.debug(`git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null} `); + this.logger.debug(`[CommandCenter][stage] git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null} `); if (!resource) { return; @@ -1382,7 +1389,7 @@ export class CommandCenter { const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked); const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved]; - this.logger.debug(`git.stage.scmResources ${scmResources.length} `); + this.logger.debug(`[CommandCenter][stage] git.stage.scmResources ${scmResources.length} `); if (!scmResources.length) { return; } @@ -2056,76 +2063,81 @@ export class CommandCenter { promptToSaveFilesBeforeCommit = 'never'; } - const enableSmartCommit = config.get('enableSmartCommit') === true; + let enableSmartCommit = config.get('enableSmartCommit') === true; const enableCommitSigning = config.get('enableCommitSigning') === true; let noStagedChanges = repository.indexGroup.resourceStates.length === 0; let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; - if (promptToSaveFilesBeforeCommit !== 'never') { - let documents = workspace.textDocuments - .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); + if (!opts.empty) { + if (promptToSaveFilesBeforeCommit !== 'never') { + let documents = workspace.textDocuments + .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); - if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) { - documents = documents - .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); - } - - if (documents.length > 0) { - const message = documents.length === 1 - ? l10n.t('The following file has unsaved changes which won\'t be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?', path.basename(documents[0].uri.fsPath)) - : l10n.t('There are {0} unsaved files.\n\nWould you like to save them before committing?', documents.length); - const saveAndCommit = l10n.t('Save All & Commit Changes'); - const commit = l10n.t('Commit Changes'); - const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); - - if (pick === saveAndCommit) { - await Promise.all(documents.map(d => d.save())); - - // After saving the dirty documents, if there are any documents that are part of the - // index group we have to add them back in order for the saved changes to be committed + if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) { documents = documents .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); - await repository.add(documents.map(d => d.uri)); + } - noStagedChanges = repository.indexGroup.resourceStates.length === 0; - noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; - } else if (pick !== commit) { - return; // do not commit on cancel + if (documents.length > 0) { + const message = documents.length === 1 + ? l10n.t('The following file has unsaved changes which won\'t be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?', path.basename(documents[0].uri.fsPath)) + : l10n.t('There are {0} unsaved files.\n\nWould you like to save them before committing?', documents.length); + const saveAndCommit = l10n.t('Save All & Commit Changes'); + const commit = l10n.t('Commit Changes'); + const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); + + if (pick === saveAndCommit) { + await Promise.all(documents.map(d => d.save())); + + // After saving the dirty documents, if there are any documents that are part of the + // index group we have to add them back in order for the saved changes to be committed + documents = documents + .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); + await repository.add(documents.map(d => d.uri)); + + noStagedChanges = repository.indexGroup.resourceStates.length === 0; + noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; + } else if (pick !== commit) { + return; // do not commit on cancel + } } } - } - // no changes, and the user has not configured to commit all in this case - if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty && !opts.all) { - const suggestSmartCommit = config.get('suggestSmartCommit') === true; + // no changes, and the user has not configured to commit all in this case + if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.all && !opts.amend) { + const suggestSmartCommit = config.get('suggestSmartCommit') === true; - if (!suggestSmartCommit) { - return; + if (!suggestSmartCommit) { + return; + } + + // prompt the user if we want to commit all or not + const message = l10n.t('There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?'); + const yes = l10n.t('Yes'); + const always = l10n.t('Always'); + const never = l10n.t('Never'); + const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never); + + if (pick === always) { + enableSmartCommit = true; + config.update('enableSmartCommit', true, true); + } else if (pick === never) { + config.update('suggestSmartCommit', false, true); + return; + } else if (pick === yes) { + enableSmartCommit = true; + } else { + // Cancel + return; + } } - // prompt the user if we want to commit all or not - const message = l10n.t('There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?'); - const yes = l10n.t('Yes'); - const always = l10n.t('Always'); - const never = l10n.t('Never'); - const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never); - - if (pick === always) { - config.update('enableSmartCommit', true, true); - } else if (pick === never) { - config.update('suggestSmartCommit', false, true); - return; - } else if (pick !== yes) { - return; // do not commit on cancel + // smart commit + if (enableSmartCommit && !opts.all) { + opts = { ...opts, all: noStagedChanges }; } } - if (opts.all === undefined) { - opts = { ...opts, all: noStagedChanges }; - } else if (!opts.all && noStagedChanges && !opts.empty) { - opts = { ...opts, all: true }; - } - // enable signing of commits if configured opts.signCommit = enableCommitSigning; @@ -2508,9 +2520,26 @@ export class CommandCenter { : l10n.t('Select a branch or tag to checkout'); quickPick.show(); - picks.push(... await createCheckoutItems(repository, opts?.detached)); - quickPick.items = [...commands, ...picks]; + + const setQuickPickItems = () => { + switch (true) { + case quickPick.value === '': + quickPick.items = [...commands, ...picks]; + break; + case commands.length === 0: + quickPick.items = picks; + break; + case picks.length === 0: + quickPick.items = commands; + break; + default: + quickPick.items = [...picks, { label: '', kind: QuickPickItemKind.Separator }, ...commands]; + break; + } + }; + + setQuickPickItems(); quickPick.busy = false; const choice = await new Promise(c => { @@ -2525,22 +2554,7 @@ export class CommandCenter { c(undefined); }))); - disposables.push(quickPick.onDidChangeValue(value => { - switch (true) { - case value === '': - quickPick.items = [...commands, ...picks]; - break; - case commands.length === 0: - quickPick.items = picks; - break; - case picks.length === 0: - quickPick.items = commands; - break; - default: - quickPick.items = [...picks, { label: '', kind: QuickPickItemKind.Separator }, ...commands]; - break; - } - })); + disposables.push(quickPick.onDidChangeValue(() => setQuickPickItems())); }); dispose(disposables); @@ -2689,6 +2703,7 @@ export class CommandCenter { { iconPath: new ThemeIcon('refresh'), tooltip: l10n.t('Regenerate Branch Name'), + location: QuickInputButtonLocation.Inline } ] : []; @@ -3037,7 +3052,8 @@ export class CommandCenter { } @command('git.fetchRef', { repository: true }) - async fetchRef(repository: Repository, ref: string): Promise { + async fetchRef(repository: Repository, ref?: string): Promise { + ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; if (!repository || !ref) { return; } @@ -3109,7 +3125,8 @@ export class CommandCenter { } @command('git.pullRef', { repository: true }) - async pullRef(repository: Repository, ref: string): Promise { + async pullRef(repository: Repository, ref?: string): Promise { + ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; if (!repository || !ref) { return; } @@ -3256,8 +3273,8 @@ export class CommandCenter { } @command('git.pushRef', { repository: true }) - async pushRef(repository: Repository, ref: string): Promise { - if (!repository || !ref) { + async pushRef(repository: Repository): Promise { + if (!repository) { return; } @@ -4136,18 +4153,75 @@ export class CommandCenter { } } - @command('git.viewCommit', { repository: true }) - async viewCommit(repository: Repository, historyItem: SourceControlHistoryItem): Promise { - if (!repository || !historyItem) { + @command('git.viewChanges', { repository: true }) + async viewChanges(repository: Repository): Promise { + await this._viewResourceGroupChanges(repository, repository.workingTreeGroup); + } + + @command('git.viewStagedChanges', { repository: true }) + async viewStagedChanges(repository: Repository): Promise { + await this._viewResourceGroupChanges(repository, repository.indexGroup); + } + + @command('git.viewUntrackedChanges', { repository: true }) + async viewUnstagedChanges(repository: Repository): Promise { + await this._viewResourceGroupChanges(repository, repository.untrackedGroup); + } + + private async _viewResourceGroupChanges(repository: Repository, resourceGroup: GitResourceGroup): Promise { + if (resourceGroup.resourceStates.length === 0) { + switch (resourceGroup.id) { + case 'index': + window.showInformationMessage(l10n.t('The repository does not have any staged changes.')); + break; + case 'workingTree': + window.showInformationMessage(l10n.t('The repository does not have any changes.')); + break; + case 'untracked': + window.showInformationMessage(l10n.t('The repository does not have any untracked changes.')); + break; + } return; } - const commit = await repository.getCommit(historyItem.id); - const title = `${historyItem.id.substring(0, 8)} - ${commit.message}`; + await commands.executeCommand('_workbench.openScmMultiDiffEditor', { + title: `${repository.sourceControl.label}: ${resourceGroup.label}`, + repositoryUri: Uri.file(repository.root), + resourceGroupId: resourceGroup.id + }); + } - const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-commit' }); + @command('git.viewCommit', { repository: true }) + async viewCommit(repository: Repository, historyItem1: SourceControlHistoryItem, historyItem2?: SourceControlHistoryItem): Promise { + if (!repository || !historyItem1) { + return; + } - await this._viewChanges(repository, historyItem, multiDiffSourceUri, title); + if (historyItem2) { + const mergeBase = await repository.getMergeBase(historyItem1.id, historyItem2.id); + if (!mergeBase || (mergeBase !== historyItem1.id && mergeBase !== historyItem2.id)) { + return; + } + } + + let title: string | undefined; + let historyItemParentId: string | undefined; + + // If historyItem2 is not provided, we are viewing a single commit. If historyItem2 is + // provided, we are viewing a range and we have to include both start and end commits. + // TODO@lszomoru - handle the case when historyItem2 is the first commit in the repository + if (!historyItem2) { + const commit = await repository.getCommit(historyItem1.id); + title = `${historyItem1.id.substring(0, 8)} - ${commit.message}`; + historyItemParentId = historyItem1.parentIds.length > 0 ? historyItem1.parentIds[0] : `${historyItem1.id}^`; + } else { + title = l10n.t('All Changes ({0} ↔ {1})', historyItem2.id.substring(0, 8), historyItem1.id.substring(0, 8)); + historyItemParentId = historyItem2.parentIds.length > 0 ? historyItem2.parentIds[0] : `${historyItem2.id}^`; + } + + const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `${historyItemParentId}..${historyItem1.id}`, { scheme: 'git-commit', }); + + await this._viewChanges(repository, historyItem1.id, historyItemParentId, multiDiffSourceUri, title); } @command('git.viewAllChanges', { repository: true }) @@ -4162,20 +4236,32 @@ export class CommandCenter { const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-changes' }); - await this._viewChanges(repository, historyItem, multiDiffSourceUri, title); + await this._viewChanges(repository, modifiedShortRef, originalShortRef, multiDiffSourceUri, title); } - async _viewChanges(repository: Repository, historyItem: SourceControlHistoryItem, multiDiffSourceUri: Uri, title: string): Promise { - const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : `${historyItem.id}^`; - const changes = await repository.diffBetween(historyItemParentId, historyItem.id); + async _viewChanges(repository: Repository, historyItemId: string, historyItemParentId: string, multiDiffSourceUri: Uri, title: string): Promise { + const changes = await repository.diffBetween(historyItemParentId, historyItemId); + const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId)); - if (changes.length === 0) { + await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); + } + + @command('git.copyCommitId', { repository: true }) + async copyCommitId(repository: Repository, historyItem: SourceControlHistoryItem): Promise { + if (!repository || !historyItem) { return; } - const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItem.id)); + env.clipboard.writeText(historyItem.id); + } - await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); + @command('git.copyCommitMessage', { repository: true }) + async copyCommitMessage(repository: Repository, historyItem: SourceControlHistoryItem): Promise { + if (!repository || !historyItem) { + return; + } + + env.clipboard.writeText(historyItem.message); } private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { @@ -4355,10 +4441,10 @@ export class CommandCenter { private getSCMResource(uri?: Uri): Resource | undefined { uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri); - this.logger.debug(`git.getSCMResource.uri ${uri && uri.toString()}`); + this.logger.debug(`[CommandCenter][getSCMResource] git.getSCMResource.uri: ${uri && uri.toString()}`); for (const r of this.model.repositories.map(r => r.root)) { - this.logger.debug(`repo root ${r}`); + this.logger.debug(`[CommandCenter][getSCMResource] repo root: ${r}`); } if (!uri) { diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 3f8553260e9..ace68c22524 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -220,16 +220,16 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider const historyProvider = this.repository.historyProvider; const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; - if (!currentHistoryItemGroup?.base) { + if (!currentHistoryItemGroup?.remote) { return []; } - const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.remote.id); if (!ancestor) { return []; } - const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); + const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.remote.id); return changes; } catch (err) { return []; diff --git a/extensions/git/src/encoding.ts b/extensions/git/src/encoding.ts index a283f628594..c80fb6ee6d5 100644 --- a/extensions/git/src/encoding.ts +++ b/extensions/git/src/encoding.ts @@ -49,15 +49,38 @@ const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { 'big5': 'cp950' }; -export function detectEncoding(buffer: Buffer): string | null { +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; } - const detected = jschardet.detect(buffer); + 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; } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 710d7a4d110..915210c4847 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1062,6 +1062,7 @@ export interface PullOptions { } export class Repository { + private _isUsingRefTable = false; constructor( private _git: Git, @@ -1113,7 +1114,7 @@ export class Repository { return result.stdout.trim(); } catch (err) { - this.logger.warn(`git config failed: ${err.message}`); + this.logger.warn(`[Git][config] git config failed: ${err.message}`); return ''; } } @@ -1165,6 +1166,16 @@ export class Repository { args.push(`--author="${options.author}"`); } + if (typeof options?.maxParents === 'number') { + args.push(`--max-parents=${options.maxParents}`); + } + + if (options?.refNames) { + args.push('--topo-order'); + args.push('--decorate=full'); + args.push(...options.refNames); + } + if (options?.path) { args.push('--', options.path); } @@ -1233,11 +1244,11 @@ export class Repository { .filter(entry => !!entry); } - async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise { + async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false, candidateGuessEncodings: string[] = []): Promise { const stdout = await this.buffer(object); if (autoGuessEncoding) { - encoding = detectEncoding(stdout) || encoding; + encoding = detectEncoding(stdout, candidateGuessEncodings) || encoding; } encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; @@ -1496,9 +1507,31 @@ export class Repository { return parseGitChanges(this.repositoryRoot, gitResult.stdout); } - async getMergeBase(ref1: string, ref2: string): Promise { + async diffTrees(treeish1: string, treeish2?: string): Promise { + const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR', treeish1]; + + if (treeish2) { + args.push(treeish2); + } + + const gitResult = await this.exec(args); + if (gitResult.exitCode) { + return []; + } + + return parseGitChanges(this.repositoryRoot, gitResult.stdout); + } + + async getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { try { - const args = ['merge-base', ref1, ref2]; + const args = ['merge-base']; + if (refs.length !== 0) { + args.push('--octopus'); + args.push(...refs); + } + + args.push(ref1, ref2); + const result = await this.exec(args); return result.stdout.trim(); @@ -2298,13 +2331,25 @@ export class Repository { } async getHEAD(): Promise { - try { - // Attempt to parse the HEAD file - const result = await this.getHEADFS(); - return result; - } - catch (err) { - this.logger.warn(err.message); + if (!this._isUsingRefTable) { + try { + // Attempt to parse the HEAD file + const result = await this.getHEADFS(); + + // Git 2.45 adds support for a new reference storage backend called "reftable", promising + // faster lookups, reads, and writes for repositories with any number of references. For + // backwards compatibility the `.git/HEAD` file contains `ref: refs/heads/.invalid`. More + // details are available at https://git-scm.com/docs/reftable + if (result.name === '.invalid') { + this._isUsingRefTable = true; + this.logger.warn(`[Git][getHEAD] Failed to parse HEAD file: Repository is using reftable format.`); + } else { + return result; + } + } + catch (err) { + this.logger.warn(`[Git][getHEAD] Failed to parse HEAD file: ${err.message}`); + } } try { @@ -2452,11 +2497,11 @@ export class Repository { remotes.push(...await this.getRemotesFS()); if (remotes.length === 0) { - this.logger.info('No remotes found in the git config file.'); + this.logger.info('[Git][getRemotes] No remotes found in the git config file'); } } catch (err) { - this.logger.warn(`getRemotes() - ${err.message}`); + this.logger.warn(`[Git][getRemotes] Error: ${err.message}`); // Fallback to using git to get the remotes remotes.push(...await this.getRemotesGit()); @@ -2525,7 +2570,7 @@ export class Repository { // On Windows and macOS ref names are case insensitive so we add --ignore-case // to handle the scenario where the user switched to a branch with incorrect // casing - if (isWindows || isMacintosh) { + if (this.git.compareGitVersionTo('2.12') !== -1 && (isWindows || isMacintosh)) { args.push('--ignore-case'); } @@ -2592,7 +2637,7 @@ export class Repository { return branch; } - this.logger.warn(`No such branch: ${name}.`); + this.logger.warn(`[Git][getBranch] No such branch: ${name}`); return Promise.reject(new Error(`No such branch: ${name}.`)); } @@ -2657,7 +2702,7 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.exec(['show', '-s', `--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'); @@ -2688,7 +2733,7 @@ export class Repository { const result = await fs.readFile(path.join(this.dotGit.path, ref), 'utf8'); return result.trim(); } catch (err) { - this.logger.warn(err.message); + this.logger.warn(`[Git][revParse] Unable to read file: ${err.message}`); } try { diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index d7f547b6b03..1d9641474e9 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, dispose, filterEvent } from './util'; import { toGitUri } from './uri'; import { Branch, RefType, UpstreamRef } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Operation } from './operation'; +import { Commit } from './git'; export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { @@ -21,6 +22,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; private _HEAD: Branch | undefined; + private _HEADMergeBase: Branch | undefined; + private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined; get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) { @@ -29,6 +32,12 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } private historyItemDecorations = new Map(); + private historyItemLabels = new Map([ + ['HEAD -> refs/heads/', 'target'], + ['refs/heads/', 'git-branch'], + ['refs/remotes/', 'cloud'], + ['refs/tags/', 'tag'] + ]); private disposables: Disposable[] = []; @@ -40,8 +49,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } private async onDidRunGitStatus(force = false): Promise { - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD:', JSON.stringify(this._HEAD)); - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - repository.HEAD:', JSON.stringify(this.repository.HEAD)); + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] HEAD:', JSON.stringify(this._HEAD)); + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] repository.HEAD:', JSON.stringify(this.repository.HEAD)); + + // Get the merge base of the current history item group + const mergeBase = await this.resolveHEADMergeBase(); // Check if HEAD has changed if (!force && @@ -49,16 +61,20 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this._HEAD?.commit === this.repository.HEAD?.commit && this._HEAD?.upstream?.name === this.repository.HEAD?.upstream?.name && this._HEAD?.upstream?.remote === this.repository.HEAD?.upstream?.remote && - this._HEAD?.upstream?.commit === this.repository.HEAD?.upstream?.commit) { - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD has not changed'); + this._HEAD?.upstream?.commit === this.repository.HEAD?.upstream?.commit && + this._HEADMergeBase?.name === mergeBase?.name && + this._HEADMergeBase?.remote === mergeBase?.remote && + this._HEADMergeBase?.commit === mergeBase?.commit) { + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] HEAD has not changed'); return; } this._HEAD = this.repository.HEAD; + this._HEADMergeBase = mergeBase; // Check if HEAD does not support incoming/outgoing (detached commit, tag) if (!this.repository.HEAD?.name || !this.repository.HEAD?.commit || this.repository.HEAD.type === RefType.Tag) { - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD does not support incoming/outgoing'); + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] HEAD does not support incoming/outgoing'); this.currentHistoryItemGroup = undefined; return; @@ -66,15 +82,23 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.currentHistoryItemGroup = { id: `refs/heads/${this.repository.HEAD.name ?? ''}`, - label: this.repository.HEAD.name ?? '', - base: this.repository.HEAD.upstream ? - { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - label: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - } : undefined + name: this.repository.HEAD.name ?? '', + revision: this.repository.HEAD.commit, + remote: this.repository.HEAD.upstream ? { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit + } : undefined, + base: mergeBase && + (mergeBase.remote !== this.repository.HEAD.upstream?.remote || + mergeBase.name !== this.repository.HEAD.upstream?.name) ? { + id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, + name: `${mergeBase.remote}/${mergeBase.name}`, + revision: mergeBase.commit + } : undefined }; - this.logger.trace(`GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup (${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup(${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); } async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise { @@ -101,8 +125,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return { id: commit.hash, parentIds: commit.parents, - label: emojify(subject), - description: commit.authorName, + message: emojify(subject), + author: commit.authorName, icon: new ThemeIcon('git-commit'), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, @@ -112,25 +136,58 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return historyItems; } - async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { - if (!historyItemParentId) { - const commit = await this.repository.getCommit(historyItemId); - historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; + async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds || typeof options.limit === 'number' || !options.limit?.id) { + return []; } + // Deduplicate refNames + const refNames = Array.from(new Set(options.historyItemGroupIds)); + + try { + // Get the common ancestor commit, and commits + const commit = await this.repository.getCommit(options.limit.id); + const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await this.repository.getEmptyTree(); + const commits = await this.repository.log({ range: `${commitParentId}..`, refNames, shortStats: true }); + + await ensureEmojis(); + + return commits.map(commit => { + const newLineIndex = commit.message.indexOf('\n'); + const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; + + const labels = this.resolveHistoryItemLabels(commit, refNames); + + return { + id: commit.hash, + parentIds: commit.parents, + message: emojify(subject), + author: commit.authorName, + icon: new ThemeIcon('git-commit'), + timestamp: commit.authorDate?.getTime(), + statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, + labels: labels.length !== 0 ? labels : undefined + }; + }); + } catch (err) { + this.logger.error(`[GitHistoryProvider][provideHistoryItems2] Failed to get history items '${options.limit.id}..': ${err}`); + return []; + } + } + + async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { + historyItemParentId = historyItemParentId ?? await this.repository.getEmptyTree(); const allChanges = await this.repository.diffBetweenShortStat(historyItemParentId, historyItemId); - return { id: historyItemId, parentIds: [historyItemParentId], label: '', statistics: allChanges }; + + return { id: historyItemId, parentIds: [historyItemParentId], message: '', statistics: allChanges }; } async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise { - if (!historyItemParentId) { - const commit = await this.repository.getCommit(historyItemId); - historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; - } + historyItemParentId = historyItemParentId ?? await this.repository.getEmptyTree(); const historyItemChangesUri: Uri[] = []; const historyItemChanges: SourceControlHistoryItemChange[] = []; - const changes = await this.repository.diffBetween(historyItemParentId, historyItemId); + const changes = await this.repository.diffTrees(historyItemParentId, historyItemId); for (const change of changes) { const historyItemUri = change.uri.with({ @@ -161,9 +218,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec async resolveHistoryItemGroupCommonAncestor(historyItemId1: string, historyItemId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { if (!historyItemId2) { - const upstreamRef = await this.resolveHistoryItemGroupBase(historyItemId1); + const upstreamRef = await this.resolveHistoryItemGroupMergeBase(historyItemId1); if (!upstreamRef) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to resolve history item group base for '${historyItemId1}'`); + this.logger.info(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to resolve history item group base for '${historyItemId1}'`); return undefined; } @@ -172,16 +229,51 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const ancestor = await this.repository.getMergeBase(historyItemId1, historyItemId2); if (!ancestor) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to resolve common ancestor for '${historyItemId1}' and '${historyItemId2}'`); + this.logger.info(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to resolve common ancestor for '${historyItemId1}' and '${historyItemId2}'`); return undefined; } try { const commitCount = await this.repository.getCommitCount(`${historyItemId1}...${historyItemId2}`); - this.logger.trace(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Resolved common ancestor for '${historyItemId1}' and '${historyItemId2}': ${JSON.stringify({ id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind })}`); + this.logger.trace(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Resolved common ancestor for '${historyItemId1}' and '${historyItemId2}': ${JSON.stringify({ id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind })}`); return { id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind }; } catch (err) { - this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to get ahead/behind for '${historyItemId1}...${historyItemId2}': ${err.message}`); + this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to get ahead/behind for '${historyItemId1}...${historyItemId2}': ${err.message}`); + } + + return undefined; + } + + async resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise { + try { + if (historyItemGroupIds.length === 0) { + // TODO@lszomoru - log + return undefined; + } else if (historyItemGroupIds.length === 1 && historyItemGroupIds[0] === this.currentHistoryItemGroup?.id) { + // Remote + if (this.currentHistoryItemGroup.remote) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.remote.id); + return ancestor; + } + + // Base + if (this.currentHistoryItemGroup.base) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.base.id); + return ancestor; + } + + // First commit + const commits = await this.repository.log({ maxParents: 0, refNames: ['HEAD'] }); + if (commits.length > 0) { + return commits[0].hash; + } + } else if (historyItemGroupIds.length > 1) { + const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], historyItemGroupIds[1], ...historyItemGroupIds.slice(2)); + return ancestor; + } + } + catch (err) { + this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor2] Failed to resolve common ancestor for ${historyItemGroupIds.join(',')}: ${err}`); } return undefined; @@ -191,7 +283,29 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } - private async resolveHistoryItemGroupBase(historyItemId: string): Promise { + private resolveHistoryItemLabels(commit: Commit, refNames: string[]): SourceControlHistoryItemLabel[] { + const labels: SourceControlHistoryItemLabel[] = []; + + for (const label of commit.refNames) { + if (!label.startsWith('HEAD -> ') && !refNames.includes(label)) { + continue; + } + + for (const [key, value] of this.historyItemLabels) { + if (label.startsWith(key)) { + labels.push({ + title: label.substring(key.length), + icon: new ThemeIcon(value) + }); + break; + } + } + } + + return labels; + } + + private async resolveHistoryItemGroupMergeBase(historyItemId: string): Promise { try { // Upstream const branch = await this.repository.getBranch(historyItemId); @@ -202,7 +316,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Base (config -> reflog -> default) const remoteBranch = await this.repository.getBranchBase(historyItemId); if (!remoteBranch?.remote || !remoteBranch?.name || !remoteBranch?.commit || remoteBranch?.type !== RefType.RemoteHead) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to resolve history item group base for '${historyItemId}'`); + this.logger.info(`[GitHistoryProvider][resolveHistoryItemGroupUpstreamOrBase] Failed to resolve history item group base for '${historyItemId}'`); return undefined; } @@ -213,12 +327,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec }; } catch (err) { - this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to get branch base for '${historyItemId}': ${err.message}`); + this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupUpstreamOrBase] Failed to get branch base for '${historyItemId}': ${err.message}`); } return undefined; } + private async resolveHEADMergeBase(): Promise { + if (this.repository.HEAD?.type !== RefType.Head || !this.repository.HEAD?.name) { + return undefined; + } + + const mergeBase = await this.repository.getBranchBase(this.repository.HEAD.name); + return mergeBase; + } + dispose(): void { dispose(this.disposables); } diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index c2d9b974be7..aa4d98adc8b 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -20,7 +20,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; -import { TerminalEnvironmentManager } from './terminal'; +import { TerminalEnvironmentManager, TerminalShellExecutionManager } from './terminal'; import { createIPCServer, IPCServer } from './ipc/ipcServer'; import { GitEditor } from './gitEditor'; import { GitPostCommitCommandsProvider } from './postCommitCommands'; @@ -48,7 +48,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, } const info = await findGit(pathHints, gitPath => { - logger.info(l10n.t('Validating found git in: "{0}"', gitPath)); + logger.info(l10n.t('[main] Validating found git in: "{0}"', gitPath)); if (excludes.length === 0) { return true; } @@ -56,7 +56,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, const normalized = path.normalize(gitPath).replace(/[\r\n]+$/, ''); const skip = excludes.some(e => normalized.startsWith(e)); if (skip) { - logger.info(l10n.t('Skipped found git in: "{0}"', gitPath)); + logger.info(l10n.t('[main] Skipped found git in: "{0}"', gitPath)); } return !skip; }, logger); @@ -66,7 +66,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, try { ipcServer = await createIPCServer(context.storagePath); } catch (err) { - logger.error(`Failed to create git IPC: ${err}`); + logger.error(`[main] Failed to create git IPC: ${err}`); } const askpass = new Askpass(ipcServer); @@ -79,7 +79,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, const terminalEnvironmentManager = new TerminalEnvironmentManager(context, [askpass, gitEditor, ipcServer]); disposables.push(terminalEnvironmentManager); - logger.info(l10n.t('Using git "{0}" from "{1}"', info.version, info.path)); + logger.info(l10n.t('[main] Using git "{0}" from "{1}"', info.version, info.path)); const git = new Git({ gitPath: info.path, @@ -113,7 +113,8 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, new GitFileSystemProvider(model), new GitDecorations(model), new GitTimelineProvider(model, cc), - new GitEditSessionIdentityProvider(model) + new GitEditSessionIdentityProvider(model), + new TerminalShellExecutionManager(model, logger) ); const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); @@ -187,7 +188,7 @@ export async function _activate(context: ExtensionContext): Promise { - logger.appendLine(l10n.t('Log level: {0}', LogLevel[logLevel])); + logger.appendLine(l10n.t('[main] Log level: {0}', LogLevel[logLevel])); }; disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel)); onDidChangeLogLevel(logger.logLevel); @@ -212,13 +213,13 @@ export async function _activate(context: ExtensionContext): Promise { + this.logger.info('[Model][doInitialScan] Initial repository scan started'); + const config = workspace.getConfiguration('git'); const autoRepositoryDetection = config.get('autoRepositoryDetection'); const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt'); + this.logger.trace(`[Model][doInitialScan] Settings: autoRepositoryDetection=${autoRepositoryDetection}, openRepositoryInParentFolders=${parentRepositoryConfig}`); + // Initial repository scan function const initialScanFn = () => Promise.all([ this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }), @@ -321,6 +325,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } */ this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length }); + this.logger.info(`[Model][doInitialScan] Initial repository scan completed - repositories (${this.repositories.length}), closed repositories (${this.closedRepositories.length}), parent repositories (${this.parentRepositories.length}), unsafe repositories (${this.unsafeRepositories.length})`); } /** @@ -329,47 +334,51 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi * the git.repositoryScanMaxDepth setting. */ private async scanWorkspaceFolders(): Promise { - const config = workspace.getConfiguration('git'); - const autoRepositoryDetection = config.get('autoRepositoryDetection'); - this.logger.trace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`); + try { + const config = workspace.getConfiguration('git'); + const autoRepositoryDetection = config.get('autoRepositoryDetection'); - if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') { - return; - } - - await Promise.all((workspace.workspaceFolders || []).map(async folder => { - const root = folder.uri.fsPath; - this.logger.trace(`[swsf] Workspace folder: ${root}`); - - // Workspace folder children - const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanMaxDepth', 1); - const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanIgnoredFolders', []); - - const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders)); - - // Repository scan folders - const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('scanRepositories') || []; - this.logger.trace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`); - - for (const scanPath of scanPaths) { - if (scanPath === '.git') { - this.logger.trace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.'); - continue; - } - - if (path.isAbsolute(scanPath)) { - const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.'); - this.logger.warn(notSupportedMessage); - console.warn(notSupportedMessage); - continue; - } - - subfolders.add(path.join(root, scanPath)); + if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') { + return; } - this.logger.trace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`); - await Promise.all([...subfolders].map(f => this.openRepository(f))); - })); + await Promise.all((workspace.workspaceFolders || []).map(async folder => { + const root = folder.uri.fsPath; + this.logger.trace(`[Model][scanWorkspaceFolders] Workspace folder: ${root}`); + + // Workspace folder children + const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanMaxDepth', 1); + const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanIgnoredFolders', []); + + const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders)); + + // Repository scan folders + const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('scanRepositories') || []; + this.logger.trace(`[Model][scanWorkspaceFolders] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`); + + for (const scanPath of scanPaths) { + if (scanPath === '.git') { + this.logger.trace('[Model][scanWorkspaceFolders] \'.git\' not supported in \'git.scanRepositories\' setting.'); + continue; + } + + if (path.isAbsolute(scanPath)) { + const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.'); + this.logger.warn(`[Model][scanWorkspaceFolders] ${notSupportedMessage}`); + console.warn(notSupportedMessage); + continue; + } + + subfolders.add(path.join(root, scanPath)); + } + + this.logger.trace(`[Model][scanWorkspaceFolders] Workspace scan sub folders: [${[...subfolders].join(', ')}]`); + await Promise.all([...subfolders].map(f => this.openRepository(f))); + })); + } + catch (err) { + this.logger.warn(`[Model][scanWorkspaceFolders] Error: ${err}`); + } } private async traverseWorkspaceFolder(workspaceFolder: string, maxDepth: number, repositoryScanIgnoredFolders: string[]): Promise { @@ -379,15 +388,26 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi while (foldersToTravers.length > 0) { const currentFolder = foldersToTravers.shift()!; + const children: fs.Dirent[] = []; + try { + children.push(...await fs.promises.readdir(currentFolder.path, { withFileTypes: true })); + + if (currentFolder.depth !== 0) { + result.push(currentFolder.path); + } + } + catch (err) { + this.logger.warn(`[Model][traverseWorkspaceFolder] Unable to read workspace folder '${currentFolder.path}': ${err}`); + continue; + } + if (currentFolder.depth < maxDepth || maxDepth === -1) { - const children = await fs.promises.readdir(currentFolder.path, { withFileTypes: true }); const childrenFolders = children .filter(dirent => dirent.isDirectory() && dirent.name !== '.git' && !repositoryScanIgnoredFolders.find(f => pathEquals(dirent.name, f))) .map(dirent => path.join(currentFolder.path, dirent.name)); - result.push(...childrenFolders); foldersToTravers.push(...childrenFolders.map(folder => { return { path: folder, depth: currentFolder.depth + 1 }; })); @@ -423,23 +443,28 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } private async onDidChangeWorkspaceFolders({ added, removed }: WorkspaceFoldersChangeEvent): Promise { - const possibleRepositoryFolders = added - .filter(folder => !this.getOpenRepository(folder.uri)); + try { + const possibleRepositoryFolders = added + .filter(folder => !this.getOpenRepository(folder.uri)); - const activeRepositoriesList = window.visibleTextEditors - .map(editor => this.getRepository(editor.document.uri)) - .filter(repository => !!repository) as Repository[]; + const activeRepositoriesList = window.visibleTextEditors + .map(editor => this.getRepository(editor.document.uri)) + .filter(repository => !!repository) as Repository[]; - const activeRepositories = new Set(activeRepositoriesList); - const openRepositoriesToDispose = removed - .map(folder => this.getOpenRepository(folder.uri)) - .filter(r => !!r) - .filter(r => !activeRepositories.has(r!.repository)) - .filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[]; + const activeRepositories = new Set(activeRepositoriesList); + const openRepositoriesToDispose = removed + .map(folder => this.getOpenRepository(folder.uri)) + .filter(r => !!r) + .filter(r => !activeRepositories.has(r!.repository)) + .filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[]; - openRepositoriesToDispose.forEach(r => r.dispose()); - this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); - await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath))); + openRepositoriesToDispose.forEach(r => r.dispose()); + this.logger.trace(`[Model][onDidChangeWorkspaceFolders] Workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); + await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath))); + } + catch (err) { + this.logger.warn(`[Model][onDidChangeWorkspaceFolders] Error: ${err}`); + } } private onDidChangeConfiguration(): void { @@ -452,50 +477,54 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi .filter(({ root }) => workspace.getConfiguration('git', root).get('enabled') !== true) .map(({ repository }) => repository); - this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); + this.logger.trace(`[Model][onDidChangeConfiguration] Workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath)); openRepositoriesToDispose.forEach(r => r.dispose()); } private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise { - if (!workspace.isTrusted) { - this.logger.trace('[svte] Workspace is not trusted.'); - return; - } - - const config = workspace.getConfiguration('git'); - const autoRepositoryDetection = config.get('autoRepositoryDetection'); - this.logger.trace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`); - - if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') { - return; - } - - await Promise.all(editors.map(async editor => { - const uri = editor.document.uri; - - if (uri.scheme !== 'file') { + try { + if (!workspace.isTrusted) { + this.logger.trace('[Model][onDidChangeVisibleTextEditors] Workspace is not trusted.'); return; } - const repository = this.getRepository(uri); + const config = workspace.getConfiguration('git'); + const autoRepositoryDetection = config.get('autoRepositoryDetection'); - if (repository) { - this.logger.trace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`); + if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') { return; } - this.logger.trace(`[svte] Open repository for editor resource ${uri.fsPath}`); - await this.openRepository(path.dirname(uri.fsPath)); - })); + await Promise.all(editors.map(async editor => { + const uri = editor.document.uri; + + if (uri.scheme !== 'file') { + return; + } + + const repository = this.getRepository(uri); + + if (repository) { + this.logger.trace(`[Model][onDidChangeVisibleTextEditors] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`); + return; + } + + this.logger.trace(`[Model][onDidChangeVisibleTextEditors] Open repository for editor resource ${uri.fsPath}`); + await this.openRepository(path.dirname(uri.fsPath)); + })); + } + catch (err) { + this.logger.warn(`[Model][onDidChangeVisibleTextEditors] Error: ${err}`); + } } @sequentialize async openRepository(repoPath: string, openIfClosed = false): Promise { - this.logger.trace(`Opening repository: ${repoPath}`); + this.logger.trace(`[Model][openRepository] Repository: ${repoPath}`); const existingRepository = await this.getRepositoryExact(repoPath); if (existingRepository) { - this.logger.trace(`Repository for path ${repoPath} already exists: ${existingRepository.root}`); + this.logger.trace(`[Model][openRepository] Repository for path ${repoPath} already exists: ${existingRepository.root}`); return; } @@ -503,7 +532,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const enabled = config.get('enabled') === true; if (!enabled) { - this.logger.trace('Git is not enabled'); + this.logger.trace('[Model][openRepository] Git is not enabled'); return; } @@ -513,7 +542,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK); const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']); if (result.stderr.trim() === '' && result.stdout.trim() === '') { - this.logger.trace(`Bare repository: ${repoPath}`); + this.logger.trace(`[Model][openRepository] Bare repository: ${repoPath}`); return; } } catch { @@ -523,16 +552,16 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi try { const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath); - this.logger.trace(`Repository root for path ${repoPath} is: ${repositoryRoot}`); + this.logger.trace(`[Model][openRepository] Repository root for path ${repoPath} is: ${repositoryRoot}`); const existingRepository = await this.getRepositoryExact(repositoryRoot); if (existingRepository) { - this.logger.trace(`Repository for path ${repositoryRoot} already exists: ${existingRepository.root}`); + this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} already exists: ${existingRepository.root}`); return; } if (this.shouldRepositoryBeIgnored(repositoryRoot)) { - this.logger.trace(`Repository for path ${repositoryRoot} is ignored`); + this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} is ignored`); return; } @@ -541,7 +570,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi if (parentRepositoryConfig !== 'always' && this.globalState.get(`parentRepository:${repositoryRoot}`) !== true) { const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot); if (isRepositoryOutsideWorkspace) { - this.logger.trace(`Repository in parent folder: ${repositoryRoot}`); + this.logger.trace(`[Model][openRepository] Repository in parent folder: ${repositoryRoot}`); if (!this._parentRepositoriesManager.hasRepository(repositoryRoot)) { // Show a notification if the parent repository is opened after the initial scan @@ -558,7 +587,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Handle unsafe repositories if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) { - this.logger.trace(`Unsafe repository: ${repositoryRoot}`); + this.logger.trace(`[Model][openRepository] Unsafe repository: ${repositoryRoot}`); // Show a notification if the unsafe repository is opened after the initial scan if (this._state === 'initialized' && !this._unsafeRepositoriesManager.hasRepository(repositoryRoot)) { @@ -572,7 +601,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Handle repositories that were closed by the user if (!openIfClosed && this._closedRepositoriesManager.isRepositoryClosed(repositoryRoot)) { - this.logger.trace(`Repository for path ${repositoryRoot} is closed`); + this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} is closed`); return; } @@ -583,12 +612,14 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this.open(repository); this._closedRepositoriesManager.deleteRepository(repository.root); + this.logger.info(`[Model][openRepository] Opened repository: ${repository.root}`); + // Do not await this, we want SCM // to know about the repo asap repository.status(); } catch (err) { // noop - this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${err}`); + this.logger.trace(`[Model][openRepository] Opening repository for path='${repoPath}' failed. Error:${err}`); } } @@ -620,7 +651,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const repositoryRootRealPath = await fs.promises.realpath(repositoryRoot); return !pathEquals(repositoryRoot, repositoryRootRealPath) ? repositoryRootRealPath : undefined; } catch (err) { - this.logger.warn(`Failed to get repository realpath for: "${repositoryRoot}". ${err}`); + this.logger.warn(`[Model][getRepositoryRootRealPath] Failed to get repository realpath for "${repositoryRoot}": ${err}`); return undefined; } } @@ -647,7 +678,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } private open(repository: Repository): void { - this.logger.info(`Open repository: ${repository.root}`); + this.logger.trace(`[Model][open] Repository: ${repository.root}`); const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed); const disappearListener = onDidDisappearRepository(() => dispose()); @@ -664,7 +695,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const checkForSubmodules = () => { if (!shouldDetectSubmodules) { - this.logger.trace('Automatic detection of git submodules is not enabled.'); + this.logger.trace('[Model][open] Automatic detection of git submodules is not enabled.'); return; } @@ -677,7 +708,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi .slice(0, submodulesLimit) .map(r => path.join(repository.root, r.path)) .forEach(p => { - this.logger.trace(`Opening submodule: '${p}'`); + this.logger.trace(`[Model][open] Opening submodule: '${p}'`); this.eventuallyScanPossibleGitRepository(p); }); }; @@ -739,7 +770,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return; } - this.logger.info(`Close repository: ${repository.root}`); + this.logger.info(`[Model][close] Repository: ${repository.root}`); this._closedRepositoriesManager.addRepository(openRepository.repository.root); openRepository.dispose(); @@ -792,7 +823,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return openRepositoryRealPath?.repository; } catch (err) { - this.logger.warn(`Failed to get repository realpath for: "${repoPath}". ${err}`); + this.logger.warn(`[Model][getRepositoryExact] Failed to get repository realpath for: "${repoPath}". Error:${err}`); return undefined; } } @@ -978,7 +1009,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this._workspaceFolders.set(workspaceFolder.uri.fsPath, result); } catch (err) { // noop - Workspace folder does not exist - this.logger.trace(`Failed to resolve workspace folder: "${workspaceFolder.uri.fsPath}". ${err}`); + this.logger.trace(`[Model][getWorkspaceFolderRealPath] Failed to resolve workspace folder "${workspaceFolder.uri.fsPath}". Error:${err}`); } } diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index 223f1945b02..13370d59bf7 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -141,7 +141,7 @@ export const Operation = { CheckoutTracking: (refLabel: string) => ({ kind: OperationKind.CheckoutTracking, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutTrackingOperation), Clean: (showProgress: boolean) => ({ kind: OperationKind.Clean, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as CleanOperation), Commit: { kind: OperationKind.Commit, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true } as CommitOperation, - Config: (readOnly: boolean) => ({ kind: OperationKind.Config, blocking: false, readOnly, remote: false, retry: false, showProgress: true } as ConfigOperation), + Config: (readOnly: boolean) => ({ kind: OperationKind.Config, blocking: false, readOnly, remote: false, retry: false, showProgress: false } as ConfigOperation), DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation, DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation, DeleteRemoteTag: { kind: OperationKind.DeleteRemoteTag, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteTagOperation, @@ -149,7 +149,7 @@ export const Operation = { Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as DiffOperation, Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation), FindTrackingBranches: { kind: OperationKind.FindTrackingBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as FindTrackingBranchesOperation, - GetBranch: { kind: OperationKind.GetBranch, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchOperation, + GetBranch: { kind: OperationKind.GetBranch, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetBranchOperation, GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation, GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation, GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation, @@ -214,7 +214,7 @@ export class OperationManager implements IOperationManager { this.operations.set(operation.kind, new Set([operation])); } - this.logger.trace(`Operation start: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); + this.logger.trace(`[OperationManager][start] ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); } end(operation: Operation): void { @@ -226,7 +226,7 @@ export class OperationManager implements IOperationManager { } } - this.logger.trace(`Operation end: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); + this.logger.trace(`[OperationManager][end] ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); } getOperations(operationKind: OperationKind): Operation[] { diff --git a/extensions/git/src/protocolHandler.ts b/extensions/git/src/protocolHandler.ts index dc73fe39965..90491fecd50 100644 --- a/extensions/git/src/protocolHandler.ts +++ b/extensions/git/src/protocolHandler.ts @@ -22,7 +22,7 @@ export class GitProtocolHandler implements UriHandler { } handleUri(uri: Uri): void { - this.logger.info(`GitProtocolHandler.handleUri(${uri.toString()})`); + this.logger.info(`[GitProtocolHandler][handleUri] URI:(${uri.toString()})`); switch (uri.path) { case '/clone': this.clone(uri); @@ -34,17 +34,17 @@ export class GitProtocolHandler implements UriHandler { const ref = data.ref; if (!data.url) { - this.logger.warn('Failed to open URI:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Failed to open URI:' + uri.toString()); return; } if (Array.isArray(data.url) && data.url.length === 0) { - this.logger.warn('Failed to open URI:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Failed to open URI:' + uri.toString()); return; } if (ref !== undefined && typeof ref !== 'string') { - this.logger.warn('Failed to open URI due to multiple references:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Failed to open URI due to multiple references:' + uri.toString()); return; } @@ -69,12 +69,12 @@ export class GitProtocolHandler implements UriHandler { } } catch (ex) { - this.logger.warn('Invalid URI:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Invalid URI:' + uri.toString()); return; } if (!(await commands.getCommands(true)).includes('git.clone')) { - this.logger.error('Could not complete git clone operation as git installation was not found.'); + this.logger.error('[GitProtocolHandler][clone] Could not complete git clone operation as git installation was not found.'); const errorMessage = l10n.t('Could not clone your repository as Git is not installed.'); const downloadGit = l10n.t('Download Git'); @@ -86,7 +86,7 @@ export class GitProtocolHandler implements UriHandler { return; } else { const cloneTarget = cloneUri.toString(true); - this.logger.info(`Executing git.clone for ${cloneTarget}`); + this.logger.info(`[GitProtocolHandler][clone] Executing git.clone for ${cloneTarget}`); commands.executeCommand('git.clone', cloneTarget, undefined, { ref: ref }); } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 7f29a213ed3..b7250d1036c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -3,27 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; import * as path from 'path'; import * as picomatch from 'picomatch'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabInputTextDiff, TabInputNotebookDiff, RelativePattern, CancellationTokenSource, LogOutputChannel, LogLevel, CancellationError, l10n } from 'vscode'; -import TelemetryReporter from '@vscode/extension-telemetry'; -import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, Remote, Status, CommitOptions, BranchQuery, FetchOptions, RefQuery, RefType } from './api/git'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, 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'; +import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefQuery, RefType, Remote, Status } from './api/git'; import { AutoFetcher } from './autofetch'; +import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, throttle } from './decorators'; -import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions, LsTreeElement } from './git'; +import { Repository as BaseRepository, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, Stash, Submodule } from './git'; +import { GitHistoryProvider } from './historyProvider'; +import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; +import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands'; +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, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; -import { IPushErrorHandlerRegistry } from './pushError'; -import { ApiRepository } from './api/api1'; -import { IRemoteSourcePublisherRegistry } from './remotePublisher'; -import { ActionButton } from './actionButton'; -import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands'; -import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; -import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; -import { GitHistoryProvider } from './historyProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -426,8 +426,8 @@ class FileEventLogger { } this.eventDisposable = combinedDisposable([ - this.onWorkspaceWorkingTreeFileChange(uri => this.logger.debug(`[wt] Change: ${uri.fsPath}`)), - this.onDotGitFileChange(uri => this.logger.debug(`[.git] Change: ${uri.fsPath}`)) + this.onWorkspaceWorkingTreeFileChange(uri => this.logger.debug(`[FileEventLogger][onWorkspaceWorkingTreeFileChange] ${uri.fsPath}`)), + this.onDotGitFileChange(uri => this.logger.debug(`[FileEventLogger][onDotGitFileChange] ${uri.fsPath}`)) ]); } @@ -478,7 +478,7 @@ class DotGitWatcher implements IFileWatcher { this.transientDisposables.push(upstreamWatcher); upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables); } catch (err) { - this.logger.warn(`Failed to watch ref '${upstreamPath}', is most likely packed.`); + this.logger.warn(`[DotGitWatcher][updateTransientWatchers] Failed to watch ref '${upstreamPath}', is most likely packed.`); } } @@ -718,6 +718,8 @@ export class Repository implements Disposable { private _untrackedGroup: SourceControlResourceGroup; get untrackedGroup(): GitResourceGroup { return this._untrackedGroup as GitResourceGroup; } + private _EMPTY_TREE: string | undefined; + private _HEAD: Branch | undefined; get HEAD(): Branch | undefined { return this._HEAD; @@ -1112,8 +1114,12 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2)); } - getMergeBase(ref1: string, ref2: string): Promise { - return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2)); + diffTrees(treeish1: string, treeish2?: string): Promise { + return this.run(Operation.Diff, () => this.repository.diffTrees(treeish1, treeish2)); + } + + getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { + return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2, ...refs)); } async hashObject(data: string): Promise { @@ -1358,22 +1364,29 @@ export class Repository implements Disposable { const config = workspace.getConfiguration('git', Uri.file(this.root)); if (!config.get('closeDiffOnOperation', false) && !ignoreSetting) { return; } - const diffEditorTabsToClose: Tab[] = []; - - for (const tab of window.tabGroups.all.map(g => g.tabs).flat()) { - const { input } = tab; - if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) { - if (input.modified.scheme === 'git' && (indexResources === undefined || indexResources.some(r => pathEquals(r, input.modified.fsPath)))) { - // Index - diffEditorTabsToClose.push(tab); - } - if (input.modified.scheme === 'file' && input.original.scheme === 'git' && (workingTreeResources === undefined || workingTreeResources.some(r => pathEquals(r, input.modified.fsPath)))) { - // Working Tree - diffEditorTabsToClose.push(tab); - } + function checkTabShouldClose(input: TabInputTextDiff | TabInputNotebookDiff) { + if (input.modified.scheme === 'git' && (indexResources === undefined || indexResources.some(r => pathEquals(r, input.modified.fsPath)))) { + // Index + return true; } + if (input.modified.scheme === 'file' && input.original.scheme === 'git' && (workingTreeResources === undefined || workingTreeResources.some(r => pathEquals(r, input.modified.fsPath)))) { + // Working Tree + return true; + } + return false; } + const diffEditorTabsToClose = window.tabGroups.all + .flatMap(g => g.tabs) + .filter(({ input }) => { + if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) { + return checkTabShouldClose(input); + } else if (input instanceof TabInputTextMultiDiff) { + return input.textDiffs.every(checkTabShouldClose); + } + return false; + }); + // Close editors window.tabGroups.close(diffEditorTabsToClose, true); } @@ -1516,7 +1529,7 @@ export class Repository implements Disposable { return upstreamBranch; } catch (err) { - this.logger.warn(`Failed to get branch details for 'refs/remotes/${branch.upstream.remote}/${branch.upstream.name}': ${err.message}.`); + this.logger.warn(`[Repository][getUpstreamBranch] Failed to get branch details for 'refs/remotes/${branch.upstream.remote}/${branch.upstream.name}': ${err.message}.`); return undefined; } } @@ -1595,23 +1608,17 @@ export class Repository implements Disposable { return await this.repository.getCommit(ref); } - async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> { - return await this.run(Operation.RevList, () => this.repository.getCommitCount(range)); - } - - async getDiff(): Promise { - const diff: string[] = []; - if (this.indexGroup.resourceStates.length !== 0) { - for (const file of this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)) { - diff.push(await this.diffIndexWithHEAD(file)); - } - } else { - for (const file of this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath)) { - diff.push(await this.diffWithHEAD(file)); - } + async getEmptyTree(): Promise { + if (!this._EMPTY_TREE) { + const result = await this.repository.exec(['hash-object', '-t', 'tree', '/dev/null']); + this._EMPTY_TREE = result.stdout.trim(); } - return diff; + return this._EMPTY_TREE; + } + + async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> { + return await this.run(Operation.RevList, () => this.repository.getCommitCount(range)); } async revParse(ref: string): Promise { @@ -1873,13 +1880,14 @@ export class Repository implements Disposable { const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); const autoGuessEncoding = configFiles.get('autoGuessEncoding'); + const candidateGuessEncodings = configFiles.get('candidateGuessEncodings'); try { - return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { const gitRelativePath = await this.repository.getGitRelativePath(ref, path); - return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } throw err; @@ -1950,7 +1958,8 @@ export class Repository implements Disposable { return await this.run(Operation.Ignore, async () => { const ignoreFile = `${this.repository.root}${path.sep}.gitignore`; const textToAppend = files - .map(uri => relativePath(this.repository.root, uri.fsPath).replace(/\\/g, '/')) + .map(uri => relativePath(this.repository.root, uri.fsPath) + .replace(/\\|\[/g, match => match === '\\' ? '/' : `\\${match}`)) .join('\n'); const document = await new Promise(c => fs.exists(ignoreFile, c)) @@ -2416,17 +2425,17 @@ export class Repository implements Disposable { const autorefresh = config.get('autorefresh'); if (!autorefresh) { - this.logger.trace('Skip running git status because autorefresh setting is disabled.'); + this.logger.trace('[Repository][onFileChange] Skip running git status because autorefresh setting is disabled.'); return; } if (this.isRepositoryHuge) { - this.logger.trace('Skip running git status because repository is huge.'); + this.logger.trace('[Repository][onFileChange] Skip running git status because repository is huge.'); return; } if (!this.operations.isIdle()) { - this.logger.trace('Skip running git status because an operation is running.'); + this.logger.trace('[Repository][onFileChange] Skip running git status because an operation is running.'); return; } diff --git a/extensions/git/src/terminal.ts b/extensions/git/src/terminal.ts index 4f6d95488bb..05a4366d0c8 100644 --- a/extensions/git/src/terminal.ts +++ b/extensions/git/src/terminal.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext, l10n, workspace } from 'vscode'; -import { filterEvent, IDisposable } from './util'; +import { ExtensionContext, l10n, LogOutputChannel, TerminalShellExecutionEndEvent, window, workspace } from 'vscode'; +import { dispose, filterEvent, IDisposable } from './util'; +import { Model } from './model'; export interface ITerminalEnvironmentProvider { featureDescription?: string; @@ -50,3 +51,42 @@ export class TerminalEnvironmentManager { this.disposable.dispose(); } } + +export class TerminalShellExecutionManager { + private readonly subcommands = new Set([ + 'add', 'branch', 'checkout', 'cherry-pick', 'clean', 'commit', 'fetch', 'merge', + 'mv', 'rebase', 'reset', 'restore', 'revert', 'rm', 'pull', 'push', 'stash', 'switch']); + + private readonly disposables: IDisposable[] = []; + + constructor( + private readonly model: Model, + private readonly logger: LogOutputChannel + ) { + window.onDidEndTerminalShellExecution(this.onDidEndTerminalShellExecution, this, this.disposables); + } + + private onDidEndTerminalShellExecution(e: TerminalShellExecutionEndEvent): void { + const { execution, exitCode, shellIntegration } = e; + const [executable, subcommand] = execution.commandLine.value.split(/\s+/); + const cwd = execution.cwd ?? shellIntegration.cwd; + + if (executable.toLowerCase() !== 'git' || !this.subcommands.has(subcommand.toLowerCase()) || !cwd || exitCode !== 0) { + return; + } + + this.logger.trace(`[TerminalShellExecutionManager][onDidEndTerminalShellExecution] Matched git subcommand: ${subcommand}`); + + const repository = this.model.getRepository(cwd); + if (!repository) { + this.logger.trace(`[TerminalShellExecutionManager][onDidEndTerminalShellExecution] Unable to find repository for current working directory: ${cwd.toString()}`); + return; + } + + repository.status(); + } + + dispose(): void { + dispose(this.disposables); + } +} diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 219c87b148d..eac6f0384b9 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -81,7 +81,7 @@ export function onceEvent(event: Event): Event { export function debounceEvent(event: Event, delay: number): Event { return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => { - let timer: NodeJS.Timer; + let timer: NodeJS.Timeout; return event(e => { clearTimeout(timer); timer = setTimeout(() => listener.call(thisArgs, e), delay); diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index d485d904215..75fb8217df7 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -18,8 +18,10 @@ "../../src/vscode-dts/vscode.proposed.scmValidation.d.ts", "../../src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts", "../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts", + "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", + "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../types/lib.textEncoder.d.ts" ] } diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index dfb24f6e7d2..a7e1a693f84 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -122,10 +122,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/picomatch@2.3.0": version "2.3.0" @@ -180,10 +182,10 @@ isexe@^3.1.1: resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +jschardet@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.3.tgz#10c2289fdae91a0aa9de8bba9c59055fd78898d3" + integrity sha512-Q1PKVMK/uu+yjdlobgWIYkUOCR1SqUmW9m/eUJNNj4zI2N12i25v8fYpVf+zCakQeaTdBdhnZTFbVIAVZIVVOg== peek-readable@^4.1.0: version "4.1.0" @@ -239,6 +241,11 @@ token-types@^4.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index d55e8dcfd03..7fcbc7f151c 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -38,7 +38,7 @@ "id": "github-enterprise" } ], - "configuration": { + "configuration": [{ "title": "GitHub Enterprise Server Authentication Provider", "properties": { "github-enterprise.uri": { @@ -46,7 +46,17 @@ "description": "GitHub Enterprise Server URI" } } + }, + { + "title": "GitHub Authentication", + "properties": { + "github.experimental.multipleAccounts": { + "type": "boolean", + "description": "Experimental support for multiple GitHub accounts" + } + } } + ] }, "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "main": "./out/extension.js", @@ -65,7 +75,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.7" }, "repository": { diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 7498a2b2202..a2497b2b0b2 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -173,6 +173,8 @@ const allFlows: IFlow[] = [ ]); if (existingLogin) { searchParams.append('login', existingLogin); + } else { + searchParams.append('prompt', 'select_account'); } // The extra toString, parse is apparently needed for env.openExternal @@ -240,6 +242,8 @@ const allFlows: IFlow[] = [ ]); if (existingLogin) { searchParams.append('login', existingLogin); + } else { + searchParams.append('prompt', 'select_account'); } const loginUrl = baseUri.with({ diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 3d73bfb7656..ed584c65f8b 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -11,7 +11,7 @@ import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './common/experimentationService'; import { Log } from './common/logger'; import { crypto } from './node/crypto'; -import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -97,6 +97,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid private readonly _keychain: Keychain; private readonly _accountsSeen = new Set(); private readonly _disposable: vscode.Disposable | undefined; + private _supportsMultipleAccounts = false; private _sessionsPromise: Promise; @@ -133,10 +134,24 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return sessions; }); + this._supportsMultipleAccounts = vscode.workspace.getConfiguration('github.experimental').get('multipleAccounts', false); + this._disposable = vscode.Disposable.from( this._telemetryReporter, - vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: false }), - this.context.secrets.onDidChange(() => this.checkForUpdates()) + vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: this._supportsMultipleAccounts }), + this.context.secrets.onDidChange(() => this.checkForUpdates()), + vscode.workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration('github.experimental.multipleAccounts')) { + const newValue = vscode.workspace.getConfiguration('github.experimental').get('multipleAccounts', false); + if (newValue === this._supportsMultipleAccounts) { + return; + } + const result = await vscode.window.showInformationMessage(vscode.l10n.t('Please reload the window to apply the new setting.'), { modal: true }, vscode.l10n.t('Reload Window')); + if (result) { + vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + } + }) ); } @@ -148,14 +163,17 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return this._sessionChangeEmitter.event; } - async getSessions(scopes?: string[]): Promise { + async getSessions(scopes: string[] | undefined, options?: vscode.AuthenticationProviderSessionOptions): Promise { // For GitHub scope list, order doesn't matter so we immediately sort the scopes const sortedScopes = scopes?.sort() || []; this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}...`); const sessions = await this._sessionsPromise; - const finalSessions = sortedScopes.length - ? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes)) + const accountFilteredSessions = options?.account + ? sessions.filter(session => session.account.label === options.account?.label) : sessions; + const finalSessions = sortedScopes.length + ? accountFilteredSessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes)) + : accountFilteredSessions; this._logger.info(`Got ${finalSessions.length} sessions for ${sortedScopes?.join(',') ?? 'all scopes'}...`); return finalSessions; @@ -226,7 +244,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid const sessionPromises = sessionData.map(async (session: SessionData) => { // For GitHub scope list, order doesn't matter so we immediately sort the scopes const scopesStr = [...session.scopes].sort().join(' '); - if (scopesSeen.has(scopesStr)) { + if (!this._supportsMultipleAccounts && scopesSeen.has(scopesStr)) { return undefined; } let userInfo: { id: string; accountName: string } | undefined; @@ -279,7 +297,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._logger.info(`Stored ${sessions.length} sessions!`); } - public async createSession(scopes: string[]): Promise { + public async createSession(scopes: string[], options?: vscode.AuthenticationProviderSessionOptions): Promise { try { // For GitHub scope list, order doesn't matter so we use a sorted scope to determine // if we've got a session already. @@ -298,51 +316,29 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid const sessions = await this._sessionsPromise; + // First we use the account specified in the options, otherwise we use the first account we have to seed auth. + const loginWith = options?.account?.label ?? sessions[0]?.account.label; + this._logger.info(`Logging in with '${loginWith ? loginWith : 'any'}' account...`); + const scopeString = sortedScopes.join(' '); - const existingLogin = sessions[0]?.account.label; - const token = await this._githubServer.login(scopeString, existingLogin); + const token = await this._githubServer.login(scopeString, loginWith); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - if (sessions.some(s => s.account.id !== session.account.id)) { - const otherAccountsIndexes = new Array(); - const otherAccountsLabels = new Set(); - for (let i = 0; i < sessions.length; i++) { - if (sessions[i].account.id !== session.account.id) { - otherAccountsIndexes.push(i); - otherAccountsLabels.add(sessions[i].account.label); - } - } - const proceed = vscode.l10n.t("Continue"); - const labelstr = [...otherAccountsLabels].join(', '); - const result = await vscode.window.showInformationMessage( - vscode.l10n.t({ - message: "You are logged into another account already ({0}).\n\nDo you want to log out of that account and log in to '{1}' instead?", - comment: ['{0} is a comma-separated list of account names. {1} is the account name to log into.'], - args: [labelstr, session.account.label] - }), - { modal: true }, - proceed - ); - if (result !== proceed) { - throw new Error(CANCELLATION_ERROR); - } - - // Remove other accounts - for (const i of otherAccountsIndexes) { - sessions.splice(i, 1); - } - } - - const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); + const sessionIndex = sessions.findIndex( + this._supportsMultipleAccounts + ? s => s.account.id === session.account.id && arrayEquals([...s.scopes].sort(), sortedScopes) + : s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes) + ); + const removed = new Array(); if (sessionIndex > -1) { - sessions.splice(sessionIndex, 1, session); + removed.push(...sessions.splice(sessionIndex, 1, session)); } else { sessions.push(session); } await this.storeSessions(sessions); - this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + this._sessionChangeEmitter.fire({ added: [session], removed, changed: [] }); this._logger.info('Login success!'); diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index af2cf22724f..c9f0a8c07d5 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -197,7 +197,7 @@ export class GitHubServer implements IGitHubServer { throw new Error(`${result.status} ${result.statusText}`); } } catch (e) { - this._logger.warn('Failed to delete token from server.' + e.message ?? e); + this._logger.warn('Failed to delete token from server.' + (e.message ?? e)); } } diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 724b304c53e..8ef2192404a 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -113,10 +113,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -182,6 +184,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-tas-client@^0.1.84: version "0.1.84" resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" diff --git a/extensions/github/package.json b/extensions/github/package.json index 5596a2c0557..ece19e32f54 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -186,7 +186,7 @@ "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index caf35ace98a..912a28439be 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -225,10 +225,12 @@ dependencies: "@octokit/openapi-types" "^17.1.0" -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -295,6 +297,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index 7b7bc3d51f9..bd8f2d6105f 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "f53c71e58787fb719399b7c38a08bceaa0c0e2d9" + "commitHash": "21f28840e04d4fa04682d19d6fe64de437f40b64" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.6.1" + "version": "0.7.5" } ], "version": 1 diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index efd69afbcd2..b8a6604de88 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.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/worlpaker/go-syntax/commit/f53c71e58787fb719399b7c38a08bceaa0c0e2d9", + "version": "https://github.com/worlpaker/go-syntax/commit/21f28840e04d4fa04682d19d6fe64de437f40b64", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -32,6 +32,9 @@ }, { "include": "#group-variables" + }, + { + "include": "#field_hover" } ] }, @@ -265,8 +268,18 @@ }, "language_constants": { "comment": "Language constants", - "match": "\\b(true|false|nil|iota)\\b", - "name": "constant.language.go" + "match": "\\b(?:(true|false)|(nil)|(iota))\\b", + "captures": { + "1": { + "name": "constant.language.boolean.go" + }, + "2": { + "name": "constant.language.null.go" + }, + "3": { + "name": "constant.language.iota.go" + } + } }, "comments": { "patterns": [ @@ -308,7 +321,7 @@ "name": "punctuation.definition.begin.bracket.square.go" } }, - "end": "(?:(\\])((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?!(?:[\\[\\]\\*]+)?\\b(?:func|struct|map)\\b)[\\w\\.\\[\\]\\*]+)?)", + "end": "(?:(\\])((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?!(?:[\\[\\]\\*]+)?\\b(?:func|struct|map)\\b)(?:[\\*\\[\\]]+)?(?:[\\w\\.]+)(?:\\[(?:(?:[\\w\\.\\*\\[\\]\\{\\}]+)(?:(?:\\,\\s*(?:[\\w\\.\\*\\[\\]\\{\\}]+))*))?\\])?)?)", "endCaptures": { "1": { "name": "punctuation.definition.end.bracket.square.go" @@ -338,7 +351,7 @@ "include": "#type-declarations-without-brackets" }, { - "include": "#generic_types" + "include": "#parameter-variable-types" }, { "include": "#functions" @@ -1285,12 +1298,15 @@ { "include": "#struct_variables_types" }, + { + "include": "#interface_variables_types" + }, { "include": "#type-declarations-without-brackets" }, { - "comment": "struct type declaration", - "match": "((?:(?:\\b\\w+\\,\\s*)+)?\\b\\w+)\\s+(?=(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\[\\]\\*]+)?\\bstruct\\b\\s*\\{)", + "comment": "struct/interface type declaration", + "match": "((?:(?:\\b\\w+\\,\\s*)+)?\\b\\w+)\\s+(?=(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\[\\]\\*]+)?\\b(?:struct|interface)\\b\\s*\\{)", "captures": { "1": { "patterns": [ @@ -1381,9 +1397,32 @@ "generic_param_types": { "comment": "generic parameter variables and types", "patterns": [ + { + "include": "#struct_variables_types" + }, + { + "include": "#interface_variables_types" + }, { "include": "#type-declarations-without-brackets" }, + { + "comment": "struct/interface type declaration", + "match": "((?:(?:\\b\\w+\\,\\s*)+)?\\b\\w+)\\s+(?=(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\[\\]\\*]+)?\\b(?:struct|interface)\\b\\s*\\{)", + "captures": { + "1": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.parameter.go" + } + ] + } + } + }, { "comment": "multiple parameters one type -with multilines", "match": "(?:(?:(?<=\\()|^\\s*)((?:(?:\\b\\w+\\,\\s*)+)(?:/(?:/|\\*).*)?)$)", @@ -1691,34 +1730,30 @@ } }, "struct_variables_types": { + "comment": "Struct variable type", + "begin": "(\\bstruct\\b)\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.struct.go" + }, + "2": { + "name": "punctuation.definition.begin.bracket.curly.go" + } + }, "patterns": [ { - "comment": "Struct variable type", - "begin": "(\\bstruct\\b)\\s*(\\{)", - "beginCaptures": { - "1": { - "name": "keyword.struct.go" - }, - "2": { - "name": "punctuation.definition.begin.bracket.curly.go" - } - }, - "patterns": [ - { - "include": "#struct_variables_types_fields" - }, - { - "include": "$self" - } - ], - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.definition.end.bracket.curly.go" - } - } + "include": "#struct_variables_types_fields" + }, + { + "include": "$self" } - ] + ], + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.end.bracket.curly.go" + } + } }, "struct_variables_types_fields": { "comment": "Struct variable type fields", @@ -1773,12 +1808,12 @@ }, { "comment": "one line with semicolon(;) without formatting gofmt - single type | property variables and types", - "match": "(?:(?<=\\{)((?:\\s*(?:(?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?(?:(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\w\\.\\*\\[\\]\\(\\)\\{\\}]+)(?:\\;)?))+)\\s*(?=\\}))", + "match": "(?:(?<=\\{)((?:\\s*(?:(?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?(?:(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\S]+)(?:\\;)?))+)\\s*(?=\\}))", "captures": { "1": { "patterns": [ { - "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\w\\.\\*\\[\\]]+)(?:\\;)?))", + "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[\\S]+)(?:\\;)?))", "captures": { "1": { "patterns": [ @@ -1827,7 +1862,7 @@ }, { "comment": "property variables and types", - "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))([\\s\\S]+))", + "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))([^\\`]+))", "captures": { "1": { "patterns": [ @@ -1859,10 +1894,11 @@ ] }, "struct_variable_types_fields_multi": { + "comment": "struct variable and type fields with multi lines", "patterns": [ { - "comment": "Struct variable for struct in struct types", - "begin": "(?:(\\w+(?:\\,\\s*\\w+)*)(?:\\s+)(?:(?:[\\[\\]\\*])+)?(\\bstruct\\b)(?:\\s*)(\\{))", + "comment": "struct in struct types", + "begin": "(?:((?:\\w+(?:\\,\\s*\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s+)(?:[\\[\\]\\*]+)?)(\\bstruct\\b)(?:\\s*)(\\{))", "beginCaptures": { "1": { "patterns": [ @@ -1896,104 +1932,208 @@ "include": "$self" } ] - } - ] - }, - "interface_variables_types": { - "patterns": [ + }, { - "comment": "interface variable types", - "begin": "(\\binterface\\b)\\s*(\\{)", + "comment": "interface in struct types", + "begin": "(?:((?:\\w+(?:\\,\\s*\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s+)(?:[\\[\\]\\*]+)?)(\\binterface\\b)(?:\\s*)(\\{))", "beginCaptures": { "1": { - "name": "keyword.interface.go" + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.other.property.go" + } + ] }, "2": { + "name": "keyword.interface.go" + }, + "3": { "name": "punctuation.definition.begin.bracket.curly.go" } }, - "patterns": [ - { - "include": "#support_functions" - }, - { - "include": "#type-declarations-without-brackets" - }, - { - "begin": "(?:([\\w\\.\\*]+)?(\\[))", - "beginCaptures": { - "1": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "(?:\\w+)", - "name": "entity.name.type.go" - } - ] - }, - "2": { - "name": "punctuation.definition.begin.bracket.square.go" - } - }, - "end": "\\]", - "endCaptures": { - "0": { - "name": "punctuation.definition.end.bracket.square.go" - } - }, - "patterns": [ - { - "include": "#generic_param_types" - } - ] - }, - { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.begin.bracket.round.go" - } - }, - "end": "\\)", - "endCaptures": { - "0": { - "name": "punctuation.definition.end.bracket.round.go" - } - }, - "patterns": [ - { - "include": "#function_param_types" - } - ] - }, - { - "comment": "other types", - "match": "([\\w\\.]+)", - "captures": { - "1": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "entity.name.type.go" - } - ] - } - } - }, - { - "include": "$self" - } - ], "end": "\\}", "endCaptures": { "0": { "name": "punctuation.definition.end.bracket.curly.go" } + }, + "patterns": [ + { + "include": "#interface_variables_types_field" + }, + { + "include": "$self" + } + ] + }, + { + "comment": "function in struct types", + "begin": "(?:((?:\\w+(?:\\,\\s*\\w+)*)(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:\\s+)(?:[\\[\\]\\*]+)?)(\\bfunc\\b)(?:\\s*)(\\())", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.other.property.go" + } + ] + }, + "2": { + "name": "keyword.function.go" + }, + "3": { + "name": "punctuation.definition.begin.bracket.round.go" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.end.bracket.round.go" + } + }, + "patterns": [ + { + "include": "#function_param_types" + }, + { + "include": "$self" + } + ] + }, + { + "comment": "one type only with multi line raw string", + "begin": "(?:((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?\\&\\|\\%\\*]+)?)+)?(\\))))", - "captures": { + "begin": "(?:(\\bmake\\b)(?:(\\()((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?:[\\[\\]\\*]+)?(?:(?!\\bmap\\b)(?:[\\w\\.]+))?(\\[(?:(?:[\\S]+)(?:(?:\\,\\s*(?:[\\S]+))*))?\\])?(?:\\,)?)?))", + "beginCaptures": { "1": { "name": "entity.name.function.support.builtin.go" }, @@ -2284,18 +2427,19 @@ "name": "entity.name.type.go" } ] - }, - "4": { - "patterns": [ - { - "include": "$self" - } - ] - }, - "5": { + } + }, + "end": "\\)", + "endCaptures": { + "0": { "name": "punctuation.definition.end.bracket.round.go" } - } + }, + "patterns": [ + { + "include": "$self" + } + ] } ] }, @@ -2321,7 +2465,7 @@ }, "switch_types": { "comment": "switch type assertions, only highlights types after case keyword", - "begin": "(?<=\\bswitch\\b)(?:\\s*)(?:(\\w+\\s*\\:\\=)?\\s*([\\w\\.\\*\\(\\)\\[\\]]+))(\\.\\(\\btype\\b\\)\\s*)(\\{)", + "begin": "(?<=\\bswitch\\b)(?:\\s*)(?:(\\w+\\s*\\:\\=)?\\s*([\\w\\.\\*\\(\\)\\[\\]\\+/\\-\\%\\<\\>\\|\\&]+))(\\.\\(\\btype\\b\\)\\s*)(\\{)", "beginCaptures": { "1": { "patterns": [ @@ -2652,7 +2796,7 @@ }, "slice_index_variables": { "comment": "slice index and capacity variables, to not scope them as property variables", - "match": "(?<=\\w\\[)((?:(?:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+\\:)|(?:\\:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+))(?:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+)?(?:\\:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+)?)(?=\\])", + "match": "(?<=\\w\\[)((?:(?:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+\\:)|(?:\\:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+))(?:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+)?(?:\\:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+)?)(?=\\])", "captures": { "1": { "patterns": [ @@ -2668,8 +2812,8 @@ } }, "property_variables": { - "comment": "Property variables in struct | parameter field in struct initialization", - "match": "(?:(?:((?:\\b[\\w\\.]+)(?:\\:(?!\\=))))(?:(?:\\s*([\\w\\.\\*\\&\\[\\]]+)(\\.\\w+)(?![\\w\\.\\*\\&\\[\\]]*(?:\\{|\\()))((?:\\s*(?:\\<|\\>|\\<\\=|\\>\\=|\\=\\=|\\!\\=|\\|\\||\\&\\&|\\+|/|\\-|\\*|\\%|\\||\\&)\\s*(?:[\\w\\.\\*\\&\\[\\]]+)(?:\\.\\w+)(?![\\w\\.\\*\\&\\[\\]]*(?:\\{|\\()))*))?)", + "comment": "Property variables in struct", + "match": "((?:\\b[\\w\\.]+)(?:\\:(?!\\=)))", "captures": { "1": { "patterns": [ @@ -2681,68 +2825,6 @@ "name": "variable.other.property.go" } ] - }, - "2": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.go" - }, - { - "include": "$self" - } - ] - }, - "3": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.property.field.go" - }, - { - "include": "$self" - } - ] - }, - "4": { - "patterns": [ - { - "match": "([\\w\\.\\*\\&\\[\\]]+)(\\.\\w+)", - "captures": { - "1": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.go" - } - ] - }, - "2": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.property.field.go" - } - ] - } - } - }, - { - "include": "$self" - } - ] } } }, @@ -2796,6 +2878,41 @@ } } }, + "field_hover": { + "comment": "struct field property and types when hovering with the mouse", + "match": "(?:(?<=^\\bfield\\b)\\s+([\\w\\*\\.]+)\\s+([\\s\\S]+))", + "captures": { + "1": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.other.property.go" + } + ] + }, + "2": { + "patterns": [ + { + "match": "\\binvalid\\b\\s+\\btype\\b", + "name": "invalid.field.go" + }, + { + "include": "#type-declarations-without-brackets" + }, + { + "include": "#parameter-variable-types" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] + } + } + }, "other_variables": { "comment": "all other variables", "match": "\\w+", diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index 6869f9ce506..ae533cc0e47 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -19,7 +19,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/grunt/src/main.ts b/extensions/grunt/src/main.ts index 886e7e27e71..fd99ba335c4 100644 --- a/extensions/grunt/src/main.ts +++ b/extensions/grunt/src/main.ts @@ -316,7 +316,7 @@ class TaskDetector { if (this.detectors.size === 0) { return Promise.resolve([]); } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTasks(); + return this.detectors.values().next().value!.getTasks(); } else { const promises: Promise[] = []; for (const detector of this.detectors.values()) { @@ -338,7 +338,7 @@ class TaskDetector { if (this.detectors.size === 0) { return undefined; } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTask(task); + return this.detectors.values().next().value!.getTask(task); } else { if ((task.scope === vscode.TaskScope.Workspace) || (task.scope === vscode.TaskScope.Global)) { return undefined; diff --git a/extensions/grunt/yarn.lock b/extensions/grunt/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/grunt/yarn.lock +++ b/extensions/grunt/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index 3e29c75fe4d..0c19b688477 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/gulp/src/main.ts b/extensions/gulp/src/main.ts index 284175741a5..b0b85ca29b9 100644 --- a/extensions/gulp/src/main.ts +++ b/extensions/gulp/src/main.ts @@ -357,7 +357,7 @@ class TaskDetector { if (this.detectors.size === 0) { return Promise.resolve([]); } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTasks(); + return this.detectors.values().next().value!.getTasks(); } else { const promises: Promise[] = []; for (const detector of this.detectors.values()) { @@ -379,7 +379,7 @@ class TaskDetector { if (this.detectors.size === 0) { return undefined; } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTask(task); + return this.detectors.values().next().value!.getTask(task); } else { if ((task.scope === vscode.TaskScope.Workspace) || (task.scope === vscode.TaskScope.Global)) { // Not supported, we don't have enough info to create the task. diff --git a/extensions/gulp/yarn.lock b/extensions/gulp/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/gulp/yarn.lock +++ b/extensions/gulp/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index 3f10e6d131f..06997d39fb0 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -8,13 +8,6 @@ import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; -declare const TextDecoder: { - new(encoding?: string): { decode(buffer: ArrayBuffer): string }; -}; - let client: AsyncDisposable | undefined; // this method is called when vs code is activated @@ -25,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; const timer = { diff --git a/extensions/html-language-features/client/tsconfig.json b/extensions/html-language-features/client/tsconfig.json index 8f5cef74fd3..349af163eea 100644 --- a/extensions/html-language-features/client/tsconfig.json +++ b/extensions/html-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 347af818273..ac026b973eb 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -259,11 +259,11 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.0", - "vscode-languageclient": "9.0.1", + "vscode-languageclient": "^10.0.0-next.8", "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/html-language-features/schemas/package.schema.json b/extensions/html-language-features/schemas/package.schema.json index ef717dbd1d1..205143c33ca 100644 --- a/extensions/html-language-features/schemas/package.schema.json +++ b/extensions/html-language-features/schemas/package.schema.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "HTML contributions to package.json", "type": "object", "properties": { "contributes": { diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 0e3ec8667ab..c1ddc242fa4 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -10,15 +10,15 @@ "main": "./out/node/htmlServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.13", - "vscode-html-languageservice": "^5.2.0", - "vscode-languageserver": "^10.0.0-next.2", + "vscode-css-languageservice": "^6.3.0", + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "npx gulp compile-extension:html-language-features-server", diff --git a/extensions/html-language-features/server/src/languageModelCache.ts b/extensions/html-language-features/server/src/languageModelCache.ts index 048d84d37cd..5dd8e439f5c 100644 --- a/extensions/html-language-features/server/src/languageModelCache.ts +++ b/extensions/html-language-features/server/src/languageModelCache.ts @@ -15,7 +15,7 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; let nModels = 0; - let cleanupInterval: NodeJS.Timer | undefined = undefined; + let cleanupInterval: NodeJS.Timeout | undefined = undefined; if (cleanupIntervalTimeInSec > 0) { cleanupInterval = setInterval(() => { const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index c8ba196769b..caaf929d895 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -7,48 +7,55 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" + integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.18": version "0.0.18" resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95" integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ== -vscode-css-languageservice@^6.2.13: - version "6.2.13" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz#c7c2dc7a081a203048d60157c65536767d6d96f8" - integrity sha512-2rKWXfH++Kxd9Z4QuEgd1IF7WmblWWU7DScuyf1YumoGLkY9DW6wF/OTlhOyO2rN63sWHX2dehIpKBbho4ZwvA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +vscode-css-languageservice@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.0.tgz#51724d193d19b1a9075b1cef5cfeea6a555d2aa4" + integrity sha512-nU92imtkgzpCL0xikrIb8WvedV553F2BENzgz23wFuok/HLN5BeQmroMy26pUwFxV2eV8oNRmYCUv8iO7kSMhw== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "3.17.5" vscode-uri "^3.0.8" -vscode-html-languageservice@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.2.0.tgz#5b36f9131acc073cebaa2074dc8ff53e84c80f31" - integrity sha512-cdNMhyw57/SQzgUUGSIMQ66jikqEN6nBNyhx5YuOyj9310+eY9zw8Q0cXpiKzDX8aHYFewQEXRnigl06j/TVwQ== +vscode-html-languageservice@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.3.0.tgz#298ae5600c6749cbb95838975d07f449c44cb478" + integrity sha512-C4Z3KsP5Ih+fjHpiBc5jxmvCl+4iEwvXegIrzu2F5pktbWvQaBT3YkVPk8N+QlSSMk8oCG6PKtZ/Sq2YHb5e8g== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.3.tgz#09d3e28e9ad12270233d07fa0b69cf1d51d7dfe4" - integrity sha512-H8ATH5SAvc3JzttS+AL6g681PiBOZM/l34WP2JZk4akY3y7NqTP+f9cJ+MhrVBbD3aDS8bdAKewZgbFLW6M8Pg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -60,17 +67,17 @@ vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== -vscode-languageserver@^10.0.0-next.2: - version "10.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.2.tgz#9a8ac58f72979961497c4fd7f6097561d4134d5f" - integrity sha512-WZdK/XO6EkNU6foYck49NpS35sahWhYFs4hwCGalH/6lhPmdUKABTnWioK/RLZKWqH8E5HdlAHQMfSBIxKBV9Q== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.3" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index 47eb287619e..aa2ea1c6840 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -128,46 +130,51 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -minimatch@^5.1.0: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== +minimatch@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" -semver@^7.3.7: +semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" -vscode-jsonrpc@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" - integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-languageclient@9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz#cdfe20267726c8d4db839dc1e9d1816e1296e854" - integrity sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== + +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: - minimatch "^5.1.0" - semver "^7.3.7" - vscode-languageserver-protocol "3.17.5" + minimatch "^9.0.3" + semver "^7.6.0" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.5: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" - integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "8.2.0" - vscode-languageserver-types "3.17.5" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.5: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" - integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index f29ed650162..d881eb8ca22 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -11,12 +11,12 @@ }, "enabledApiProposals": [ "documentPaste", - "diffContentOptions", - "dropMetadata" + "diffContentOptions" ], "activationEvents": [ "onNotebook:jupyter-notebook", - "onNotebookSerializer:interactive" + "onNotebookSerializer:interactive", + "onNotebookSerializer:repl" ], "extensionKind": [ "workspace", @@ -62,6 +62,11 @@ "command": "notebook.cellOutput.copy", "title": "%copyCellOutput.title%", "category": "Notebook" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "title": "%openCellOutput.title%", + "category": "Notebook" } ], "notebooks": [ @@ -108,12 +113,24 @@ { "command": "notebook.cellOutput.copy", "when": "notebookCellHasOutputs" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "when": "false" } ], "webview/context": [ { "command": "notebook.cellOutput.copy", "when": "webviewId == 'notebook.output' && webviewSection == 'image'" + }, + { + "command": "notebook.cellOutput.copy", + "when": "webviewId == 'notebook.output' && webviewSection == 'text'" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "when": "webviewId == 'notebook.output' && webviewSection == 'text'" } ] } diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index af7d8f4ab47..7a3d95181cf 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -7,6 +7,7 @@ "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", "cleanInvalidImageAttachment.title": "Clean Invalid Image Attachment Reference", "copyCellOutput.title": "Copy Cell Output", + "openCellOutput.title": "Open Cell Output in Text Editor", "markdownAttachmentRenderer.displayName": { "message": "Markdown-It ipynb Cell Attachment renderer", "comment": [ diff --git a/extensions/ipynb/src/common.ts b/extensions/ipynb/src/common.ts index a25973e95a6..d81951d2d7c 100644 --- a/extensions/ipynb/src/common.ts +++ b/extensions/ipynb/src/common.ts @@ -67,5 +67,5 @@ export interface CellMetadata { } export function useCustomPropertyInMetadata() { - return !workspace.getConfiguration('jupyter', undefined).get('experimental.dropCustomMetadata', false); + return !workspace.getConfiguration('jupyter', undefined).get('experimental.dropCustomMetadata', true); } diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 889f4c07445..6d73107ef54 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -117,13 +117,6 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(cleaner); } - // Update new file contribution - vscode.extensions.onDidChange(() => { - vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); - }); - vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); - - return { get dropCustomMetadata() { return !useCustomPropertyInMetadata(); diff --git a/extensions/ipynb/src/notebookImagePaste.ts b/extensions/ipynb/src/notebookImagePaste.ts index 7ea63e7026a..5e188dd554a 100644 --- a/extensions/ipynb/src/notebookImagePaste.ts +++ b/extensions/ipynb/src/notebookImagePaste.ts @@ -48,7 +48,7 @@ function getImageMimeType(uri: vscode.Uri): string | undefined { class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider { - public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'image', 'attachment'); + public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'image', 'attachment'); async provideDocumentPasteEdits( document: vscode.TextDocument, @@ -68,7 +68,7 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod } const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment'), DropOrPasteEditProvider.kind); - pasteEdit.yieldTo = [vscode.DocumentPasteEditKind.Empty.append('text')]; + pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text')]; pasteEdit.additionalEdit = insert.additionalEdit; return [pasteEdit]; } @@ -85,7 +85,7 @@ class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscod } const dropEdit = new vscode.DocumentDropEdit(insert.insertText); - dropEdit.yieldTo = [vscode.DocumentPasteEditKind.Empty.append('text')]; + dropEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text')]; dropEdit.additionalEdit = insert.additionalEdit; dropEdit.title = vscode.l10n.t('Insert Image as Attachment'); return dropEdit; diff --git a/extensions/ipynb/src/notebookModelStoreSync.ts b/extensions/ipynb/src/notebookModelStoreSync.ts index 737034266f0..a3266216498 100644 --- a/extensions/ipynb/src/notebookModelStoreSync.ts +++ b/extensions/ipynb/src/notebookModelStoreSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext, NotebookCellKind, NotebookDocument, NotebookDocumentChangeEvent, NotebookEdit, workspace, WorkspaceEdit, type NotebookCell, type NotebookDocumentWillSaveEvent } from 'vscode'; +import { Disposable, ExtensionContext, NotebookCellKind, NotebookDocument, NotebookDocumentChangeEvent, NotebookEdit, workspace, WorkspaceEdit, type NotebookCell, type NotebookDocumentWillSaveEvent } from 'vscode'; import { getCellMetadata, getVSCodeCellLanguageId, removeVSCodeCellLanguageId, setVSCodeCellLanguageId, sortObjectPropertiesRecursively } from './serializers'; import { CellMetadata, useCustomPropertyInMetadata } from './common'; import { getNotebookMetadata } from './notebookSerializer'; @@ -28,8 +28,57 @@ export function activate(context: ExtensionContext) { workspace.onWillSaveNotebookDocument(waitForPendingModelUpdates, undefined, context.subscriptions); } +type NotebookDocumentChangeEventEx = Omit; +let mergedEvents: NotebookDocumentChangeEventEx | undefined; +let timer: NodeJS.Timeout; + +function triggerDebouncedNotebookDocumentChangeEvent() { + if (timer) { + clearTimeout(timer); + } + if (!mergedEvents) { + return; + } + const args = mergedEvents; + mergedEvents = undefined; + onDidChangeNotebookCells(args); +} + +export function debounceOnDidChangeNotebookDocument() { + const disposable = workspace.onDidChangeNotebookDocument(e => { + if (!isSupportedNotebook(e.notebook)) { + return; + } + if (!mergedEvents) { + mergedEvents = e; + } else if (mergedEvents.notebook === e.notebook) { + // Same notebook, we can merge the updates. + mergedEvents = { + cellChanges: e.cellChanges.concat(mergedEvents.cellChanges), + contentChanges: e.contentChanges.concat(mergedEvents.contentChanges), + notebook: e.notebook + }; + } else { + // Different notebooks, we cannot merge the updates. + // Hence we need to process the previous notebook and start a new timer for the new notebook. + triggerDebouncedNotebookDocumentChangeEvent(); + // Start a new timer for the new notebook. + mergedEvents = e; + } + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(triggerDebouncedNotebookDocumentChangeEvent, 200); + }); + + + return Disposable.from(disposable, new Disposable(() => { + clearTimeout(timer); + })); +} + function isSupportedNotebook(notebook: NotebookDocument) { - return notebook.notebookType === 'jupyter-notebook' || notebook.notebookType === 'interactive'; + return notebook.notebookType === 'jupyter-notebook'; } function waitForPendingModelUpdates(e: NotebookDocumentWillSaveEvent) { @@ -37,6 +86,7 @@ function waitForPendingModelUpdates(e: NotebookDocumentWillSaveEvent) { return; } + triggerDebouncedNotebookDocumentChangeEvent(); const promises = pendingNotebookCellModelUpdates.get(e.notebook); if (!promises) { return; @@ -78,7 +128,7 @@ function trackAndUpdateCellMetadata(notebook: NotebookDocument, updates: { cell: promise.then(clean, clean); } -function onDidChangeNotebookCells(e: NotebookDocumentChangeEvent) { +function onDidChangeNotebookCells(e: NotebookDocumentChangeEventEx) { if (!isSupportedNotebook(e.notebook)) { return; } diff --git a/extensions/ipynb/tsconfig.json b/extensions/ipynb/tsconfig.json index 189a4848f56..2a6cc47eeeb 100644 --- a/extensions/ipynb/tsconfig.json +++ b/extensions/ipynb/tsconfig.json @@ -7,8 +7,6 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts", - "../../src/vscode-dts/vscode.proposed.dropMetadata.d.ts" - + "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts" ] } diff --git a/extensions/jake/package.json b/extensions/jake/package.json index 637d417e503..1d5d1250db0 100644 --- a/extensions/jake/package.json +++ b/extensions/jake/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/jake/src/main.ts b/extensions/jake/src/main.ts index 33d39e288a4..a2511dc62df 100644 --- a/extensions/jake/src/main.ts +++ b/extensions/jake/src/main.ts @@ -290,7 +290,7 @@ class TaskDetector { if (this.detectors.size === 0) { return Promise.resolve([]); } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTasks(); + return this.detectors.values().next().value!.getTasks(); } else { const promises: Promise[] = []; for (const detector of this.detectors.values()) { @@ -312,7 +312,7 @@ class TaskDetector { if (this.detectors.size === 0) { return undefined; } else if (this.detectors.size === 1) { - return this.detectors.values().next().value.getTask(task); + return this.detectors.values().next().value!.getTask(task); } else { if ((task.scope === vscode.TaskScope.Workspace) || (task.scope === vscode.TaskScope.Global)) { // Not supported, we don't have enough info to create the task. diff --git a/extensions/jake/yarn.lock b/extensions/jake/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/jake/yarn.lock +++ b/extensions/jake/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index 12f6e5cac1f..f7c332337cb 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -111,14 +111,17 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" + "pattern": "^\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { - "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" }, // e.g. * ...| or */| or *-----*/| "unIndentedLinePattern": { "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$" + }, + "indentNextLinePattern": { + "pattern": "^((.*=>\\s*)|((.*[^\\w]+|\\s*)(if|while|for)\\s*\\(.*\\)\\s*))$" } }, "onEnterRules": [ @@ -197,6 +200,33 @@ "action": { "indent": "outdent" } - } + }, + // Indent when pressing enter from inside () + { + "beforeText": "^.*\\([^\\)]*$", + "afterText": "^\\s*\\).*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside {} + { + "beforeText": "^.*\\{[^\\}]*$", + "afterText": "^\\s*\\}.*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside [] + { + "beforeText": "^.*\\[[^\\]]*$", + "afterText": "^\\s*\\].*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, ] } diff --git a/extensions/javascript/snippets/javascript.code-snippets b/extensions/javascript/snippets/javascript.code-snippets index 9448fd140d9..5bf6aa5edee 100644 --- a/extensions/javascript/snippets/javascript.code-snippets +++ b/extensions/javascript/snippets/javascript.code-snippets @@ -1,16 +1,79 @@ { - "define module": { - "prefix": "define", + "Constructor": { + "prefix": "ctor", "body": [ - "define([", - "\t'require',", - "\t'${1:dependency}'", - "], function(require, ${2:factory}) {", - "\t'use strict';", + "/**", + " *", + " */", + "constructor() {", + "\tsuper();", "\t$0", - "});" + "}" ], - "description": "define module" + "description": "Constructor" + }, + "Class Definition": { + "prefix": "class", + "isFileTemplate": true, + "body": [ + "class ${1:name} {", + "\tconstructor(${2:parameters}) {", + "\t\t$0", + "\t}", + "}" + ], + "description": "Class Definition" + }, + "Method Definition": { + "prefix": "method", + "body": [ + "/**", + " * ", + " */", + "${1:name}() {", + "\t$0", + "}" + ], + "description": "Method Definition" + }, + "Import Statement": { + "prefix": "import", + "body": [ + "import { $0 } from \"${1:module}\";" + ], + "description": "Import external module" + }, + "Log to the console": { + "prefix": "log", + "body": [ + "console.log($1);", + "$0" + ], + "description": "Log to the console" + }, + "Log warning to console": { + "prefix": "warn", + "body": [ + "console.warn($1);", + "$0" + ], + "description": "Log warning to the console" + }, + "Log error to console": { + "prefix": "error", + "body": [ + "console.error($1);", + "$0" + ], + "description": "Log error to the console" + }, + "Throw Exception": { + "prefix": "throw", + "body": [ + "throw new Error(\"$1\");", + "$0" + ], + "description": "Throw Exception" }, "For Loop": { "prefix": "for", @@ -22,20 +85,20 @@ ], "description": "For Loop" }, - "For-Each Loop": { - "prefix": "foreach", + "For-Each Loop using =>": { + "prefix": "foreach =>", "body": [ "${1:array}.forEach(${2:element} => {", "\t$TM_SELECTED_TEXT$0", "});" ], - "description": "For-Each Loop" + "description": "For-Each Loop using =>" }, "For-In Loop": { "prefix": "forin", "body": [ "for (const ${1:key} in ${2:object}) {", - "\tif (Object.hasOwnProperty.call(${2:object}, ${1:key})) {", + "\tif (Object.prototype.hasOwnProperty.call(${2:object}, ${1:key})) {", "\t\tconst ${3:element} = ${2:object}[${1:key}];", "\t\t$TM_SELECTED_TEXT$0", "\t}", @@ -46,12 +109,21 @@ "For-Of Loop": { "prefix": "forof", "body": [ - "for (const ${1:iterator} of ${2:object}) {", + "for (const ${1:element} of ${2:object}) {", "\t$TM_SELECTED_TEXT$0", "}" ], "description": "For-Of Loop" }, + "For-Await-Of Loop": { + "prefix": "forawaitof", + "body": [ + "for await (const ${1:element} of ${2:object}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "For-Await-Of Loop" + }, "Function Statement": { "prefix": "function", "body": [ @@ -149,13 +221,6 @@ ], "description": "Set Interval Function" }, - "Import Statement": { - "prefix": "import", - "body": [ - "import { $0 } from \"${1:module}\";" - ], - "description": "Import external module" - }, "Region Start": { "prefix": "#region", "body": [ @@ -170,27 +235,6 @@ ], "description": "Folding Region End" }, - "Log to the console": { - "prefix": "log", - "body": [ - "console.log($1);" - ], - "description": "Log to the console" - }, - "Log warning to console": { - "prefix": "warn", - "body": [ - "console.warn($1);" - ], - "description": "Log warning to the console" - }, - "Log error to console": { - "prefix": "error", - "body": [ - "console.error($1);" - ], - "description": "Log error to the console" - }, "new Promise": { "prefix": "newpromise", "body": [ @@ -199,5 +243,23 @@ "})" ], "description": "Create a new Promise" + }, + "Async Function Statement": { + "prefix": "async function", + "body": [ + "async function ${1:name}(${2:params}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "Async Function Statement" + }, + "Async Function Expression": { + "prefix": "async arrow function", + "body": [ + "async (${1:params}) => {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "Async Function Expression" } } diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f78f494d727..91ed937fe6f 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -8,12 +8,6 @@ import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; - -declare function fetch(uri: string, options: any): any; - let client: AsyncDisposable | undefined; // this method is called when vs code is activated @@ -24,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; const schemaRequests: SchemaRequestService = { diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index f892664d917..90aafc89b84 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -8,7 +8,8 @@ export type JSONLanguageStatus = { schemas: string[] }; import { workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, - ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n + ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n, + RelativePattern } from 'vscode'; import { LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind, @@ -360,18 +361,29 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa const schemaDocuments: { [uri: string]: boolean } = {}; // handle content request - client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { + client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => { const uri = Uri.parse(uriPath); + const uriString = uri.toString(); if (uri.scheme === 'untitled') { - return Promise.reject(new ResponseError(3, l10n.t('Unable to load {0}', uri.toString()))); + throw new ResponseError(3, l10n.t('Unable to load {0}', uriString)); } - if (uri.scheme !== 'http' && uri.scheme !== 'https') { - return workspace.openTextDocument(uri).then(doc => { - schemaDocuments[uri.toString()] = true; - return doc.getText(); - }, error => { - return Promise.reject(new ResponseError(2, error.toString())); - }); + if (uri.scheme === 'vscode') { + try { + runtime.logOutputChannel.info('read schema from vscode: ' + uriString); + ensureFilesystemWatcherInstalled(uri); + const content = await workspace.fs.readFile(uri); + return new TextDecoder().decode(content); + } catch (e) { + throw new ResponseError(5, e.toString(), e); + } + } else if (uri.scheme !== 'http' && uri.scheme !== 'https') { + try { + const document = await workspace.openTextDocument(uri); + schemaDocuments[uriString] = true; + return document.getText(); + } catch (e) { + throw new ResponseError(2, e.toString(), e); + } } else if (schemaDownloadEnabled) { if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { /* __GDPR__ @@ -381,13 +393,15 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa "schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." } } */ - runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath }); + runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriString }); + } + try { + return await runtime.schemaRequests.getContent(uriString); + } catch (e) { + throw new ResponseError(4, e.toString()); } - return runtime.schemaRequests.getContent(uriPath).catch(e => { - return Promise.reject(new ResponseError(4, e.toString())); - }); } else { - return Promise.reject(new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload))); + throw new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); } }); @@ -415,15 +429,50 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa schemaResolutionErrorStatusBarItem.hide(); } }; - - toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); - toDispose.push(workspace.onDidCloseTextDocument(d => { - const uriString = d.uri.toString(); + const handleContentClosed = (uriString: string) => { if (handleContentChange(uriString)) { delete schemaDocuments[uriString]; } fileSchemaErrors.delete(uriString); + }; + + const watchers: Map = new Map(); + toDispose.push(new Disposable(() => { + for (const d of watchers.values()) { + d.dispose(); + } })); + + + const ensureFilesystemWatcherInstalled = (uri: Uri) => { + + const uriString = uri.toString(); + if (!watchers.has(uriString)) { + try { + const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, '*')); + const handleChange = (uri: Uri) => { + runtime.logOutputChannel.info('schema change detected ' + uri.toString()); + client.sendNotification(SchemaContentChangeNotification.type, uriString); + }; + const createListener = watcher.onDidCreate(handleChange); + const changeListener = watcher.onDidChange(handleChange); + const deleteListener = watcher.onDidDelete(() => { + const watcher = watchers.get(uriString); + if (watcher) { + watcher.dispose(); + watchers.delete(uriString); + } + }); + watchers.set(uriString, Disposable.from(watcher, createListener, changeListener, deleteListener)); + } catch { + runtime.logOutputChannel.info('Problem installing a file system watcher for ' + uriString); + } + } + }; + + toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); + toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString()))); + toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); const handleRetryResolveSchemaCommand = () => { diff --git a/extensions/json-language-features/client/tsconfig.json b/extensions/json-language-features/client/tsconfig.json index aa51e4d0157..89e6a6c12b7 100644 --- a/extensions/json-language-features/client/tsconfig.json +++ b/extensions/json-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 541106ae1a0..fa8004e2b02 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -163,10 +163,10 @@ "dependencies": { "@vscode/extension-telemetry": "^0.9.0", "request-light": "^0.7.0", - "vscode-languageclient": "9.0.1" + "vscode-languageclient": "^10.0.0-next.8" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 7d50c250b0d..8472ca618a4 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -15,13 +15,13 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.2.1", "request-light": "^0.7.0", - "vscode-json-languageservice": "^5.3.10", - "vscode-languageserver": "^10.0.0-next.2", + "vscode-json-languageservice": "^5.4.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "prepublishOnly": "npm run clean && npm run compile", diff --git a/extensions/json-language-features/server/src/languageModelCache.ts b/extensions/json-language-features/server/src/languageModelCache.ts index 17ffe2add4f..441a5a19b28 100644 --- a/extensions/json-language-features/server/src/languageModelCache.ts +++ b/extensions/json-language-features/server/src/languageModelCache.ts @@ -15,7 +15,7 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; let nModels = 0; - let cleanupInterval: NodeJS.Timer | undefined = undefined; + let cleanupInterval: NodeJS.Timeout | undefined = undefined; if (cleanupIntervalTimeInSec > 0) { cleanupInterval = setInterval(() => { const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; @@ -79,4 +79,4 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime } } }; -} \ No newline at end of file +} diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 598fe822994..608619637e4 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" + integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.18": version "0.0.18" @@ -22,56 +24,66 @@ jsonc-parser@^3.2.1: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== +jsonc-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.0.tgz#030d182672c8ffc2805db95467c83ffc0b033d9d" + integrity sha512-RK1Xb5alM78sdXpB2hqqK7jxAE5jTRH05GvUiLWqh7Vbp6OPHuJYlsAMRUDYNYJTAQgkmhHgkdwOEknxwP4ojQ== + request-light@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== -vscode-json-languageservice@^5.3.10: - version "5.3.10" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.3.10.tgz#7d56872cbb7460baf0491cea31807e537244dbae" - integrity sha512-KlbUYaer3DAnsVyRtgg/MhXOu4TTwY8TjaZYRY7Mt80zSpmvbmd58YT4Wq2ZiqHzdioD6lAvRSxhSCL0DvVY8Q== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +vscode-json-languageservice@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.4.0.tgz#caf1aabc81b1df9faf6a97e4c34e13a2d10a8cdf" + integrity sha512-NCkkCr63OHVkE4lcb0xlUAaix6vE5gHQW4NrswbLEh3ArXj81lrGuFTsGEYEUXlNHdnc53vWPcjeSy/nMTrfXg== dependencies: "@vscode/l10n" "^0.0.18" - jsonc-parser "^3.2.1" + jsonc-parser "^3.3.0" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.3.tgz#09d3e28e9ad12270233d07fa0b69cf1d51d7dfe4" - integrity sha512-H8ATH5SAvc3JzttS+AL6g681PiBOZM/l34WP2JZk4akY3y7NqTP+f9cJ+MhrVBbD3aDS8bdAKewZgbFLW6M8Pg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-languageserver-types@^3.17.5: version "3.17.5" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver@^10.0.0-next.2: - version "10.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.2.tgz#9a8ac58f72979961497c4fd7f6097561d4134d5f" - integrity sha512-WZdK/XO6EkNU6foYck49NpS35sahWhYFs4hwCGalH/6lhPmdUKABTnWioK/RLZKWqH8E5HdlAHQMfSBIxKBV9Q== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.3" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index df4818e025b..c825de07683 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -128,10 +130,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -minimatch@^5.1.0: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== +minimatch@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" @@ -140,39 +142,44 @@ request-light@^0.7.0: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== -semver@^7.3.7: +semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" -vscode-jsonrpc@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" - integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-languageclient@9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz#cdfe20267726c8d4db839dc1e9d1816e1296e854" - integrity sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== + +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: - minimatch "^5.1.0" - semver "^7.3.7" - vscode-languageserver-protocol "3.17.5" + minimatch "^9.0.3" + semver "^7.6.0" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.5: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" - integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "8.2.0" - vscode-languageserver-types "3.17.5" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.5: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" - integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== yallist@^4.0.0: version "4.0.0" diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index 70a5f39e2aa..b5d8a03be09 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", - "commitHash": "ca657621631c568fdce35fb583c97b8d0b7a226c" + "commitHash": "c686684f18153687886e7d19c1bfc3a33076b1ab" } }, "license": "MIT", - "version": "0.22.1" + "version": "0.23.0" } ], "version": 1 diff --git a/extensions/julia/syntaxes/julia.tmLanguage.json b/extensions/julia/syntaxes/julia.tmLanguage.json index 7b63d2e8222..f66fda97f70 100644 --- a/extensions/julia/syntaxes/julia.tmLanguage.json +++ b/extensions/julia/syntaxes/julia.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/JuliaEditorSupport/atom-language-julia/commit/ca657621631c568fdce35fb583c97b8d0b7a226c", + "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/c686684f18153687886e7d19c1bfc3a33076b1ab", "name": "Julia", "scopeName": "source.julia", "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", @@ -321,7 +321,7 @@ "name": "keyword.control.export.julia" }, { - "match": "\\b(?|<-|-->|=>)", + "match": "\\.?(?:<-->|->|-->|<--|←|→|↔|↚|↛|↞|↠|↢|↣|↦|↤|↮|⇎|⇍|⇏|⇐|⇒|⇔|⇴|⇶|⇷|⇸|⇹|⇺|⇻|⇼|⇽|⇾|⇿|⟵|⟶|⟷|⟹|⟺|⟻|⟼|⟽|⟾|⟿|⤀|⤁|⤂|⤃|⤄|⤅|⤆|⤇|⤌|⤍|⤎|⤏|⤐|⤑|⤔|⤕|⤖|⤗|⤘|⤝|⤞|⤟|⤠|⥄|⥅|⥆|⥇|⥈|⥊|⥋|⥎|⥐|⥒|⥓|⥖|⥗|⥚|⥛|⥞|⥟|⥢|⥤|⥦|⥧|⥨|⥩|⥪|⥫|⥬|⥭|⥰|⧴|⬱|⬰|⬲|⬳|⬴|⬵|⬶|⬷|⬸|⬹|⬺|⬻|⬼|⬽|⬾|⬿|⭀|⭁|⭂|⭃|⥷|⭄|⥺|⭇|⭈|⭉|⭊|⭋|⭌|←|→|⇜|⇝|↜|↝|↩|↪|↫|↬|↼|↽|⇀|⇁|⇄|⇆|⇇|⇉|⇋|⇌|⇚|⇛|⇠|⇢|↷|↶|↺|↻|=>)", "name": "keyword.operator.arrow.julia" }, { @@ -394,7 +394,7 @@ } }, { - "match": "(?:===|∈|\\.∈|∉|\\.∉|∋|\\.∋|∌|\\.∌|≈|\\.≈|≉|\\.≉|≠|\\.≠|≡|\\.≡|≢|\\.≢|⊆|\\.⊆|⊇|\\.⊇|⊈|\\.⊈|⊉|\\.⊉|⊊|\\.⊊|⊋|\\.⊋|\\.==|!==|!=|\\.>=|\\.>|\\.<=|\\.<|\\.≤|\\.≥|==|\\.!=|\\.=|\\.!|<:|>:|:>|(?)>=|(?|<|≥|≤)", + "match": "(\\.?((?)>=|>|<|≥|≤|===|==|≡|!=|≠|!==|≢|∈|∉|∋|∌|⊆|⊈|⊂|⊄|⊊|∝|∊|∍|∥|∦|∷|∺|∻|∽|∾|≁|≃|≂|≄|≅|≆|≇|≈|≉|≊|≋|≌|≍|≎|≐|≑|≒|≓|≖|≗|≘|≙|≚|≛|≜|≝|≞|≟|≣|≦|≧|≨|≩|≪|≫|≬|≭|≮|≯|≰|≱|≲|≳|≴|≵|≶|≷|≸|≹|≺|≻|≼|≽|≾|≿|⊀|⊁|⊃|⊅|⊇|⊉|⊋|⊏|⊐|⊑|⊒|⊜|⊩|⊬|⊮|⊰|⊱|⊲|⊳|⊴|⊵|⊶|⊷|⋍|⋐|⋑|⋕|⋖|⋗|⋘|⋙|⋚|⋛|⋜|⋝|⋞|⋟|⋠|⋡|⋢|⋣|⋤|⋥|⋦|⋧|⋨|⋩|⋪|⋫|⋬|⋭|⋲|⋳|⋴|⋵|⋶|⋷|⋸|⋹|⋺|⋻|⋼|⋽|⋾|⋿|⟈|⟉|⟒|⦷|⧀|⧁|⧡|⧣|⧤|⧥|⩦|⩧|⩪|⩫|⩬|⩭|⩮|⩯|⩰|⩱|⩲|⩳|⩵|⩶|⩷|⩸|⩹|⩺|⩻|⩼|⩽|⩾|⩿|⪀|⪁|⪂|⪃|⪄|⪅|⪆|⪇|⪈|⪉|⪊|⪋|⪌|⪍|⪎|⪏|⪐|⪑|⪒|⪓|⪔|⪕|⪖|⪗|⪘|⪙|⪚|⪛|⪜|⪝|⪞|⪟|⪠|⪡|⪢|⪣|⪤|⪥|⪦|⪧|⪨|⪩|⪪|⪫|⪬|⪭|⪮|⪯|⪰|⪱|⪲|⪳|⪴|⪵|⪶|⪷|⪸|⪹|⪺|⪻|⪼|⪽|⪾|⪿|⫀|⫁|⫂|⫃|⫄|⫅|⫆|⫇|⫈|⫉|⫊|⫋|⫌|⫍|⫎|⫏|⫐|⫑|⫒|⫓|⫔|⫕|⫖|⫗|⫘|⫙|⫷|⫸|⫹|⫺|⊢|⊣|⟂|⫪|⫫|<:|>:))", "name": "keyword.operator.relation.julia" }, { @@ -418,11 +418,11 @@ "name": "keyword.operator.applies.julia" }, { - "match": "(?:\\||\\.\\||\\&|\\.\\&|~|\\.~|⊻|\\.⊻)", + "match": "(?:\\||\\.\\||\\&|\\.\\&|~|¬|\\.~|⊻|\\.⊻)", "name": "keyword.operator.bitwise.julia" }, { - "match": "(?:\\+\\+|--|\\+|\\.\\+|-|\\.\\-|\\*|\\.\\*|//(?!=)|\\.//(?!=)|/|\\./|%|\\.%|\\\\|\\.\\\\|\\^|\\.\\^|÷|\\.÷|⋅|\\.⋅|∩|\\.∩|∪|\\.∪|×|√|∛)", + "match": "\\.?(?:\\+\\+|\\-\\-|\\+|\\-|−|¦|\\||⊕|⊖|⊞|⊟|∪|∨|⊔|±|∓|∔|∸|≏|⊎|⊻|⊽|⋎|⋓|⟇|⧺|⧻|⨈|⨢|⨣|⨤|⨥|⨦|⨧|⨨|⨩|⨪|⨫|⨬|⨭|⨮|⨹|⨺|⩁|⩂|⩅|⩊|⩌|⩏|⩐|⩒|⩔|⩖|⩗|⩛|⩝|⩡|⩢|⩣|\\*|//?|⌿|÷|%|&|·|·|⋅|∘|×|\\\\|∩|∧|⊗|⊘|⊙|⊚|⊛|⊠|⊡|⊓|∗|∙|∤|⅋|≀|⊼|⋄|⋆|⋇|⋉|⋊|⋋|⋌|⋏|⋒|⟑|⦸|⦼|⦾|⦿|⧶|⧷|⨇|⨰|⨱|⨲|⨳|⨴|⨵|⨶|⨷|⨸|⨻|⨼|⨽|⩀|⩃|⩄|⩋|⩍|⩎|⩑|⩓|⩕|⩘|⩚|⩜|⩞|⩟|⩠|⫛|⊍|▷|⨝|⟕|⟖|⟗|⨟|\\^|↑|↓|⇵|⟰|⟱|⤈|⤉|⤊|⤋|⤒|⤓|⥉|⥌|⥍|⥏|⥑|⥔|⥕|⥘|⥙|⥜|⥝|⥠|⥡|⥣|⥥|⥮|⥯|↑|↓|√|∛|∜|⋆|±|∓)", "name": "keyword.operator.arithmetic.julia" }, { @@ -438,7 +438,7 @@ "name": "keyword.operator.relation.in.julia" }, { - "match": "(?:\\.(?=(?:@|_|\\p{L}))|\\.\\.+)", + "match": "(?:\\.(?=(?:@|_|\\p{L}))|\\.\\.+|…|⁝|⋮|⋱|⋰|⋯)", "name": "keyword.operator.dots.julia" }, { diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index 965df91bed4..3c7203d5d2a 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "45c7b12ee68563afd50407e5eac02d30d33dbe7a" + "commitHash": "969429cb9230a63f9155987f069acd4234d10e1a" } }, "license": "MIT", - "version": "1.6.0", + "version": "1.9.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/extensions/latex/syntaxes/Bibtex.tmLanguage.json b/extensions/latex/syntaxes/Bibtex.tmLanguage.json index 31568ec10aa..0f3a3a408a5 100644 --- a/extensions/latex/syntaxes/Bibtex.tmLanguage.json +++ b/extensions/latex/syntaxes/Bibtex.tmLanguage.json @@ -4,18 +4,18 @@ "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/jlelong/vscode-latex-basics/commit/36411b38cf4ed18e02050249e2162b1316488686", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/c787db94a56bd93131ce0938046063320a02cc73", "name": "BibTeX", "scopeName": "text.bibtex", "comment": "Grammar based on description from https://github.com/aclements/biblib\n", "patterns": [ { - "match": "@(?i:comment)(?=[\\s{(])", "captures": { "0": { "name": "punctuation.definition.comment.bibtex" } }, + "match": "@(?i:comment)(?=[\\s{(])", "name": "comment.block.at-sign.bibtex" }, { @@ -235,18 +235,18 @@ "include": "#string_var" }, { - "name": "keyword.operator.bibtex", - "match": "#" + "match": "#", + "name": "keyword.operator.bibtex" } ] }, "integer": { - "match": "\\s*(\\d+)\\s*", "captures": { "1": { "name": "constant.numeric.bibtex" } - } + }, + "match": "\\s*(\\d+)\\s*" }, "nested_braces": { "begin": "\\{", @@ -267,14 +267,6 @@ } ] }, - "string_var": { - "match": "[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*", - "captures": { - "0": { - "name": "support.variable.bibtex" - } - } - }, "string_content": { "patterns": [ { @@ -316,6 +308,14 @@ ] } ] + }, + "string_var": { + "captures": { + "0": { + "name": "support.variable.bibtex" + } + }, + "match": "[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*" } } } \ No newline at end of file diff --git a/extensions/latex/syntaxes/LaTeX.tmLanguage.json b/extensions/latex/syntaxes/LaTeX.tmLanguage.json index 9805c80cfa5..bc97a73bda8 100644 --- a/extensions/latex/syntaxes/LaTeX.tmLanguage.json +++ b/extensions/latex/syntaxes/LaTeX.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/jlelong/vscode-latex-basics/commit/c863108952ce5c695c066ea8dc498135abb19e0a", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/969429cb9230a63f9155987f069acd4234d10e1a", "name": "LaTeX", "scopeName": "text.tex.latex", "patterns": [ @@ -41,13 +41,13 @@ "name": "punctuation.definition.arguments.begin.latex" } }, + "contentName": "support.class.latex", "end": "(\\})", "endCaptures": { "0": { "name": "punctuation.definition.arguments.end.latex" } }, - "contentName": "support.class.latex", "patterns": [ { "include": "$self" @@ -94,7 +94,7 @@ "4": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -116,14 +116,12 @@ "include": "text.tex#braces" }, { - "include": "$base" + "include": "$self" } ] }, { - "name": "meta.function.environment.songs.latex", "begin": "((?:\\s*)\\\\begin\\{songs\\}\\{.*\\})", - "end": "(\\\\end\\{songs\\}(?:\\s*\\n)?)", "captures": { "1": { "patterns": [ @@ -134,20 +132,22 @@ } }, "contentName": "meta.data.environment.songs.latex", + "end": "(\\\\end\\{songs\\}(?:\\s*\\n)?)", + "name": "meta.function.environment.songs.latex", "patterns": [ { - "name": "meta.chord.block.latex support.class.chord.block.environment.latex", "begin": "\\\\\\[", "end": "\\]", + "name": "meta.chord.block.latex support.class.chord.block.environment.latex", "patterns": [ { - "include": "$base" + "include": "$self" } ] }, { - "name": "meta.chord.block.latex support.class.chord.block.environment.latex", - "match": "\\^" + "match": "\\^", + "name": "meta.chord.block.latex support.class.chord.block.environment.latex" }, { "include": "$self" @@ -156,7 +156,6 @@ }, { "begin": "(?:^\\s*)?\\\\begin\\{(lstlisting|minted|pyglist)\\}(?=\\[|\\{)", - "end": "\\\\end\\{\\1\\}", "captures": { "0": { "patterns": [ @@ -166,31 +165,11 @@ ] } }, + "end": "\\\\end\\{\\1\\}", "patterns": [ { "include": "#multiline-optional-arg-no-highlight" }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)((?:c|cpp))(\\})", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - }, - "2": { - "name": "variable.parameter.function.latex" - }, - "3": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", - "contentName": "source.cpp.embedded.latex", - "patterns": [ - { - "include": "source.cpp.embedded.latex" - } - ] - }, { "begin": "(?:\\G|(?<=\\]))(\\{)((?:asy|asymptote))(\\})", "beginCaptures": { @@ -212,6 +191,48 @@ } ] }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)((?:bash))(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", + "contentName": "source.shell", + "patterns": [ + { + "include": "source.shell" + } + ] + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)((?:c|cpp))(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", + "contentName": "source.cpp.embedded.latex", + "patterns": [ + { + "include": "source.cpp.embedded.latex" + } + ] + }, { "begin": "(?:\\G|(?<=\\]))(\\{)((?:css))(\\})", "beginCaptures": { @@ -233,6 +254,27 @@ } ] }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)((?:gnuplot))(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", + "contentName": "source.gnuplot", + "patterns": [ + { + "include": "source.gnuplot" + } + ] + }, { "begin": "(?:\\G|(?<=\\]))(\\{)((?:hs|haskell))(\\})", "beginCaptures": { @@ -275,27 +317,6 @@ } ] }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)((?:xml))(\\})", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - }, - "2": { - "name": "variable.parameter.function.latex" - }, - "3": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", - "contentName": "text.xml", - "patterns": [ - { - "include": "text.xml" - } - ] - }, { "begin": "(?:\\G|(?<=\\]))(\\{)((?:java))(\\})", "beginCaptures": { @@ -317,27 +338,6 @@ } ] }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)((?:lua))(\\})", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - }, - "2": { - "name": "variable.parameter.function.latex" - }, - "3": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", - "contentName": "source.lua", - "patterns": [ - { - "include": "source.lua" - } - ] - }, { "begin": "(?:\\G|(?<=\\]))(\\{)((?:jl|julia))(\\})", "beginCaptures": { @@ -359,27 +359,6 @@ } ] }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)((?:rb|ruby))(\\})", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - }, - "2": { - "name": "variable.parameter.function.latex" - }, - "3": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", - "contentName": "source.ruby", - "patterns": [ - { - "include": "source.ruby" - } - ] - }, { "begin": "(?:\\G|(?<=\\]))(\\{)((?:js|javascript))(\\})", "beginCaptures": { @@ -402,7 +381,7 @@ ] }, { - "begin": "(?:\\G|(?<=\\]))(\\{)((?:ts|typescript))(\\})", + "begin": "(?:\\G|(?<=\\]))(\\{)((?:lua))(\\})", "beginCaptures": { "1": { "name": "punctuation.definition.arguments.begin.latex" @@ -415,15 +394,15 @@ } }, "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", - "contentName": "source.ts", + "contentName": "source.lua", "patterns": [ { - "include": "source.ts" + "include": "source.lua" } ] }, { - "begin": "(?:\\G|(?<=\\]))(\\{)((?:py|python))(\\})", + "begin": "(?:\\G|(?<=\\]))(\\{)((?:py|python|sage))(\\})", "beginCaptures": { "1": { "name": "punctuation.definition.arguments.begin.latex" @@ -444,7 +423,7 @@ ] }, { - "begin": "(?:\\G|(?<=\\]))(\\{)((?:yaml))(\\})", + "begin": "(?:\\G|(?<=\\]))(\\{)((?:rb|ruby))(\\})", "beginCaptures": { "1": { "name": "punctuation.definition.arguments.begin.latex" @@ -457,10 +436,10 @@ } }, "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", - "contentName": "source.yaml", + "contentName": "source.ruby", "patterns": [ { - "include": "source.yaml" + "include": "source.ruby" } ] }, @@ -485,6 +464,69 @@ } ] }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)((?:ts|typescript))(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", + "contentName": "source.ts", + "patterns": [ + { + "include": "source.ts" + } + ] + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)((?:xml))(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", + "contentName": "text.xml", + "patterns": [ + { + "include": "text.xml" + } + ] + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)((?:yaml))(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "end": "^\\s*(?=\\\\end\\{(?:minted|lstlisting|pyglist)\\})", + "contentName": "source.yaml", + "patterns": [ + { + "include": "source.yaml" + } + ] + }, { "begin": "(?:\\G|(?<=\\]))(\\{)([a-zA-Z]*)(\\})", "beginCaptures": { @@ -504,436 +546,6 @@ } ] }, - { - "begin": "\\s*\\\\begin\\{(?:cppcode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:cppcode)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:cppcode)\\*?\\})", - "contentName": "source.cpp.embedded.latex", - "patterns": [ - { - "include": "source.cpp.embedded.latex" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:hscode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:hscode)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:hscode)\\*?\\})", - "contentName": "source.haskell", - "patterns": [ - { - "include": "source.haskell" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:luacode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:luacode)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:luacode)\\*?\\})", - "contentName": "source.lua", - "patterns": [ - { - "include": "source.lua" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\})", - "contentName": "source.julia", - "patterns": [ - { - "include": "source.julia" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:juliacode|juliaverbatim|juliablock|juliaconcode|juliaconsole|juliaconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:juliacode|juliaverbatim|juliablock|juliaconcode|juliaconsole|juliaconverbatim)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:juliacode|juliaverbatim|juliablock|juliaconcode|juliaconsole|juliaconverbatim)\\*?\\})", - "contentName": "source.julia", - "patterns": [ - { - "include": "source.julia" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:sageblock|sagesilent|sageverbatim|sageexample|sagecommandline|python|pythonq|pythonrepl)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:sageblock|sagesilent|sageverbatim|sageexample|sagecommandline|python|pythonq|pythonrepl)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:sageblock|sagesilent|sageverbatim|sageexample|sagecommandline|python|pythonq|pythonrepl)\\*?\\})", - "contentName": "source.python", - "patterns": [ - { - "include": "source.python" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:pycode|pyverbatim|pyblock|pyconcode|pyconsole|pyconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:pycode|pyverbatim|pyblock|pyconcode|pyconsole|pyconverbatim)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:pycode|pyverbatim|pyblock|pyconcode|pyconsole|pyconverbatim)\\*?\\})", - "contentName": "source.python", - "patterns": [ - { - "include": "source.python" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:pylabcode|pylabverbatim|pylabblock|pylabconcode|pylabconsole|pylabconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:pylabcode|pylabverbatim|pylabblock|pylabconcode|pylabconsole|pylabconverbatim)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:pylabcode|pylabverbatim|pylabblock|pylabconcode|pylabconsole|pylabconverbatim)\\*?\\})", - "contentName": "source.python", - "patterns": [ - { - "include": "source.python" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:sympycode|sympyverbatim|sympyblock|sympyconcode|sympyconsole|sympyconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:sympycode|sympyverbatim|sympyblock|sympyconcode|sympyconsole|sympyconverbatim)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:sympycode|sympyverbatim|sympyblock|sympyconcode|sympyconsole|sympyconverbatim)\\*?\\})", - "contentName": "source.python", - "patterns": [ - { - "include": "source.python" - } - ] - } - ] - }, - { - "begin": "\\s*\\\\begin\\{(?:scalacode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", - "end": "\\s*\\\\end\\{(?:scalacode)\\*?\\}", - "captures": { - "0": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - }, - "patterns": [ - { - "include": "#multiline-optional-arg-no-highlight" - }, - { - "begin": "(?:\\G|(?<=\\]))(\\{)", - "beginCaptures": { - "1": { - "name": "punctuation.definition.arguments.begin.latex" - } - }, - "end": "(\\})", - "endCaptures": { - "1": { - "name": "punctuation.definition.arguments.end.latex" - } - }, - "contentName": "variable.parameter.function.latex" - }, - { - "begin": "^(?=\\s*)", - "end": "^\\s*(?=\\\\end\\{(?:scalacode)\\*?\\})", - "contentName": "source.scala", - "patterns": [ - { - "include": "source.scala" - } - ] - } - ] - }, { "begin": "\\s*\\\\begin\\{(?:asy|asycode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", "end": "\\s*\\\\end\\{(?:asy|asycode)\\*?\\}", @@ -977,6 +589,49 @@ } ] }, + { + "begin": "\\s*\\\\begin\\{(?:cppcode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:cppcode)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:cppcode)\\*?\\})", + "contentName": "source.cpp.embedded.latex", + "patterns": [ + { + "include": "source.cpp.embedded.latex" + } + ] + } + ] + }, { "begin": "\\s*\\\\begin\\{(?:dot2tex|dotcode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", "end": "\\s*\\\\end\\{(?:dot2tex|dotcode)\\*?\\}", @@ -1063,9 +718,395 @@ } ] }, + { + "begin": "\\s*\\\\begin\\{(?:hscode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:hscode)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:hscode)\\*?\\})", + "contentName": "source.haskell", + "patterns": [ + { + "include": "source.haskell" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\})", + "contentName": "source.julia", + "patterns": [ + { + "include": "source.julia" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:juliacode|juliaverbatim|juliablock|juliaconcode|juliaconsole|juliaconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:juliacode|juliaverbatim|juliablock|juliaconcode|juliaconsole|juliaconverbatim)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:juliacode|juliaverbatim|juliablock|juliaconcode|juliaconsole|juliaconverbatim)\\*?\\})", + "contentName": "source.julia", + "patterns": [ + { + "include": "source.julia" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:luacode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:luacode)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:luacode)\\*?\\})", + "contentName": "source.lua", + "patterns": [ + { + "include": "source.lua" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:pycode|pyverbatim|pyblock|pyconcode|pyconsole|pyconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:pycode|pyverbatim|pyblock|pyconcode|pyconsole|pyconverbatim)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:pycode|pyverbatim|pyblock|pyconcode|pyconsole|pyconverbatim)\\*?\\})", + "contentName": "source.python", + "patterns": [ + { + "include": "source.python" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:pylabcode|pylabverbatim|pylabblock|pylabconcode|pylabconsole|pylabconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:pylabcode|pylabverbatim|pylabblock|pylabconcode|pylabconsole|pylabconverbatim)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:pylabcode|pylabverbatim|pylabblock|pylabconcode|pylabconsole|pylabconverbatim)\\*?\\})", + "contentName": "source.python", + "patterns": [ + { + "include": "source.python" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:sageblock|sagesilent|sageverbatim|sageexample|sagecommandline|python|pythonq|pythonrepl)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:sageblock|sagesilent|sageverbatim|sageexample|sagecommandline|python|pythonq|pythonrepl)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:sageblock|sagesilent|sageverbatim|sageexample|sagecommandline|python|pythonq|pythonrepl)\\*?\\})", + "contentName": "source.python", + "patterns": [ + { + "include": "source.python" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:scalacode)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:scalacode)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:scalacode)\\*?\\})", + "contentName": "source.scala", + "patterns": [ + { + "include": "source.scala" + } + ] + } + ] + }, + { + "begin": "\\s*\\\\begin\\{(?:sympycode|sympyverbatim|sympyblock|sympyconcode|sympyconsole|sympyconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:sympycode|sympyverbatim|sympyblock|sympyconcode|sympyconsole|sympyconverbatim)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:sympycode|sympyverbatim|sympyblock|sympyconcode|sympyconsole|sympyconverbatim)\\*?\\})", + "contentName": "source.python", + "patterns": [ + { + "include": "source.python" + } + ] + } + ] + }, { "begin": "\\s*\\\\begin\\{([a-zA-Z]*code|lstlisting|minted|pyglist)\\*?\\}(?:\\[.*\\])?(?:\\{.*\\})?", - "end": "\\\\end\\{\\1\\}(?:\\s*\\n)?", "captures": { "0": { "patterns": [ @@ -1076,11 +1117,744 @@ } }, "contentName": "meta.function.embedded.latex", + "end": "\\\\end\\{\\1\\}(?:\\s*\\n)?", "name": "meta.embedded.block.generic.latex" }, + { + "begin": "((?:^\\s*)?\\\\begin\\{((?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?))\\})(?:\\[[^\\]]*\\]){,2}(?=\\{)", + "captures": { + "1": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "end": "(\\\\end\\{\\2\\})", + "patterns": [ + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:asy|asymptote)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.asy", + "patterns": [ + { + "include": "source.asy" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:bash)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.shell", + "patterns": [ + { + "include": "source.shell" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:c|cpp)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.cpp.embedded.latex", + "patterns": [ + { + "include": "source.cpp.embedded.latex" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:css)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.css", + "patterns": [ + { + "include": "source.css" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:gnuplot)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.gnuplot", + "patterns": [ + { + "include": "source.gnuplot" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:hs|haskell)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.haskell", + "patterns": [ + { + "include": "source.haskell" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:html)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "text.html", + "patterns": [ + { + "include": "text.html.basic" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:java)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.java", + "patterns": [ + { + "include": "source.java" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:jl|julia)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.julia", + "patterns": [ + { + "include": "source.julia" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:js|javascript)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.js", + "patterns": [ + { + "include": "source.js" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:lua)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.lua", + "patterns": [ + { + "include": "source.lua" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:py|python|sage)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.python", + "patterns": [ + { + "include": "source.python" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:rb|ruby)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.ruby", + "patterns": [ + { + "include": "source.ruby" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:rust)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.rust", + "patterns": [ + { + "include": "source.rust" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:ts|typescript)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.ts", + "patterns": [ + { + "include": "source.ts" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:xml)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "text.xml", + "patterns": [ + { + "include": "text.xml" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:yaml)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "source.yaml", + "patterns": [ + { + "include": "source.yaml" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)(?:__|[a-z\\s]*)(?i:tikz|tikzpicture)", + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "contentName": "text.tex.latex", + "patterns": [ + { + "include": "text.tex.latex" + } + ] + } + ] + }, + { + "begin": "\\G(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "patterns": [ + { + "begin": "\\G", + "end": "(\\})\\s*$", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "patterns": [ + { + "include": "text.tex#braces" + }, + { + "include": "$self" + } + ] + }, + { + "begin": "^(\\s*)", + "contentName": "meta.function.embedded.latex", + "end": "^\\s*(?=\\\\end\\{(?:RobExt)?(?:CacheMeCode|PlaceholderPathFromCode\\*?|PlaceholderFromCode\\*?|SetPlaceholderCode\\*?)\\})", + "name": "meta.embedded.block.generic.latex" + } + ] + } + ] + }, { "begin": "(?:^\\s*)?\\\\begin\\{(terminal\\*?)\\}(?=\\[|\\{)", - "end": "\\\\end\\{\\1\\}", "captures": { "0": { "patterns": [ @@ -1090,6 +1864,7 @@ ] } }, + "end": "\\\\end\\{\\1\\}", "patterns": [ { "include": "#multiline-optional-arg-no-highlight" @@ -1125,7 +1900,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -1135,7 +1910,7 @@ "5": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -1143,6 +1918,7 @@ "name": "punctuation.definition.arguments.begin.latex" } }, + "end": "\\s*(\\};)", "patterns": [ { "begin": "%", @@ -1157,8 +1933,7 @@ { "include": "source.gnuplot" } - ], - "end": "\\s*(\\};)" + ] }, { "begin": "(\\s*\\\\begin\\{((?:fboxv|boxedv|V|v|spv)erbatim\\*?)\\})", @@ -1232,13 +2007,13 @@ "name": "meta.function.verbatim.latex" }, { - "comment": "Captures \\command[option]{url}{optional category}{optional name}{text}", "begin": "(?:\\s*)((\\\\)(?:href|hyperref|hyperimage))(?=\\[|\\{)", "beginCaptures": { "1": { "name": "support.function.url.latex" } }, + "comment": "Captures \\command[option]{url}{optional category}{optional name}{text}", "end": "(\\})", "endCaptures": { "1": { @@ -1266,13 +2041,13 @@ "name": "punctuation.definition.arguments.begin.latex" } }, + "contentName": "meta.variable.parameter.function.latex", "end": "(?=\\})", "patterns": [ { - "include": "$base" + "include": "$self" } - ], - "contentName": "meta.variable.parameter.function.latex" + ] }, { "begin": "(?:\\G|(?<=\\]))(?:(\\{)[^}]*(\\}))?(\\{)", @@ -1287,19 +2062,17 @@ "name": "punctuation.definition.arguments.begin.latex" } }, + "contentName": "meta.variable.parameter.function.latex", "end": "(?=\\})", "patterns": [ { - "include": "$base" + "include": "$self" } - ], - "contentName": "meta.variable.parameter.function.latex" + ] } ] }, { - "match": "(?:\\s*)((\\\\)url)(\\{)([^}]*)(\\})", - "name": "meta.function.link.url.latex", "captures": { "1": { "name": "support.function.url.latex" @@ -1316,25 +2089,25 @@ "'": { "name": "markup.underline.link.latex" } - } + }, + "match": "(?:\\s*)((\\\\)url)(\\{)([^}]*)(\\})", + "name": "meta.function.link.url.latex" }, { + "captures": { + "1": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, "comment": "These two patterns match the \\begin{document} and \\end{document} commands, so that the environment matching pattern following them will ignore those commands.", "match": "(\\s*\\\\begin\\{document\\})", - "name": "meta.function.begin-document.latex", - "captures": { - "1": { - "patterns": [ - { - "include": "#begin-env-tokenizer" - } - ] - } - } + "name": "meta.function.begin-document.latex" }, { - "match": "(\\s*\\\\end\\{document\\})", - "name": "meta.function.end-document.latex", "captures": { "1": { "patterns": [ @@ -1343,7 +2116,9 @@ } ] } - } + }, + "match": "(\\s*\\\\end\\{document\\})", + "name": "meta.function.end-document.latex" }, { "begin": "(?:\\s*)((\\\\)begin)(\\{)((?:\\+?array|equation|(?:IEEE)?eqnarray|multline|align|aligned|alignat|alignedat|flalign|flaligned|flalignat|split|gather|gathered|\\+?cases|(?:display)?math|\\+?[a-zA-Z]*matrix|[pbBvV]?NiceMatrix|[pbBvV]?NiceArray|(?:(?:arg)?(?:mini|maxi)))(?:\\*|!)?)(\\})(\\s*\\n)?", @@ -1383,7 +2158,7 @@ "include": "text.tex#math" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1417,7 +2192,7 @@ "include": "text.tex#math" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1445,7 +2220,7 @@ "name": "keyword.control.table.newline.latex" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1464,7 +2239,7 @@ "name": "meta.function.environment.list.latex", "patterns": [ { - "include": "$base" + "include": "$self" } ] }, @@ -1483,7 +2258,7 @@ "name": "meta.function.environment.latex.tikz", "patterns": [ { - "include": "$base" + "include": "$self" } ] }, @@ -1502,7 +2277,7 @@ "name": "meta.function.environment.frame.latex", "patterns": [ { - "include": "$base" + "include": "$self" } ] }, @@ -1531,8 +2306,8 @@ ] } }, - "end": "(\\\\end\\{markdown\\})", "contentName": "meta.embedded.markdown_latex_combined", + "end": "(\\\\end\\{markdown\\})", "patterns": [ { "include": "text.tex.markdown_latex_combined" @@ -1554,7 +2329,7 @@ "name": "meta.function.environment.general.latex", "patterns": [ { - "include": "$base" + "include": "$self" } ] }, @@ -1593,7 +2368,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -1613,7 +2388,7 @@ "include": "text.tex#braces" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1629,7 +2404,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -1649,7 +2424,7 @@ "include": "text.tex#braces" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1679,7 +2454,7 @@ "include": "text.tex#braces" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1710,7 +2485,7 @@ "include": "text.tex#braces" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1740,7 +2515,7 @@ "include": "text.tex#braces" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1770,7 +2545,7 @@ "include": "text.tex#braces" }, { - "include": "$base" + "include": "$self" } ] }, @@ -1787,7 +2562,7 @@ "name": "meta.scope.item.latex" }, { - "begin": "((\\\\)(?:[aA]uto|foot|full|no|ref|short|[tT]ext|[pP]aren|[sS]mart)?[cC]ite(?:al)?(?:p|s|t|author|year(?:par)?|title)?[ANP]*\\*?)((?:(?:\\([^\\)]*\\)){0,2}(?:\\[[^\\]]*\\]){0,2}\\{[\\p{Alphabetic}:.]*\\})*)(?:([<\\[])[^\\]<>]*([>\\]]))?(?:(\\[)[^\\]]*(\\]))?(\\{)", + "begin": "((\\\\)(?:[aA]uto|foot|full|no|ref|short|[tT]ext|[pP]aren|[sS]mart)?[cC]ite(?:al)?(?:p|s|t|author|year(?:par)?|title)?[ANP]*\\*?)((?:(?:\\([^\\)]*\\)){0,2}(?:\\[[^\\]]*\\]){0,2}\\{[\\p{Alphabetic}\\p{Number}_:.-]*\\})*)(<[^\\]<>]*>)?((?:\\[[^\\]]*\\])*)(\\{)", "captures": { "1": { "name": "keyword.control.cite.latex" @@ -1803,18 +2578,20 @@ ] }, "4": { - "name": "punctuation.definition.arguments.optional.begin.latex" + "patterns": [ + { + "include": "#optional-arg-angle-no-highlight" + } + ] }, "5": { - "name": "punctuation.definition.arguments.optional.end.latex" + "patterns": [ + { + "include": "#optional-arg-bracket-no-highlight" + } + ] }, "6": { - "name": "punctuation.definition.arguments.optional.begin.latex" - }, - "7": { - "name": "punctuation.definition.arguments.optional.end.latex" - }, - "8": { "name": "punctuation.definition.arguments.begin.latex" } }, @@ -1827,6 +2604,7 @@ "name": "meta.citation.latex", "patterns": [ { + "match": "((%).*)$", "captures": { "1": { "name": "comment.line.percentage.tex" @@ -1834,8 +2612,7 @@ "2": { "name": "punctuation.definition.comment.tex" } - }, - "match": "((%).*)$" + } }, { "match": "[\\p{Alphabetic}\\p{Number}:.-]+", @@ -1871,7 +2648,7 @@ ] }, { - "begin": "((\\\\)(?:\\w*[rR]ef\\*?))(\\{)", + "begin": "((\\\\)(?:\\w*[rR]ef\\*?))(?:\\[[^\\]]*\\])?(\\{)", "beginCaptures": { "1": { "name": "keyword.control.ref.latex" @@ -1965,7 +2742,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2008,7 +2785,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2045,7 +2822,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2092,7 +2869,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2128,6 +2905,32 @@ "match": "((\\\\)(?:jl|julia)[cv]?)((?:\\[[^\\[]*?\\])?)(?:(?:([^a-zA-Z\\{])(.*?)(\\4))|(?:(\\{)(.*?)(\\})))", "name": "meta.function.verb.latex" }, + { + "begin": "((\\\\)(?:directlua|luadirect))(\\{)", + "beginCaptures": { + "1": { + "name": "support.function.verb.latex" + }, + "2": { + "name": "punctuation.definition.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "source.lua", + "patterns": [ + { + "include": "source.lua" + } + ] + }, { "match": "\\\\(?:newline|pagebreak|clearpage|linebreak|pause)(?:\\b)", "name": "keyword.control.layout.latex" @@ -2151,7 +2954,7 @@ "include": "text.tex#math" }, { - "include": "$base" + "include": "$self" } ] }, @@ -2178,7 +2981,7 @@ "include": "text.tex#math" }, { - "include": "$base" + "include": "$self" } ] }, @@ -2205,7 +3008,7 @@ "include": "text.tex#math" }, { - "include": "$base" + "include": "$self" } ] }, @@ -2228,7 +3031,7 @@ "include": "text.tex#math" }, { - "include": "$base" + "include": "$self" } ] }, @@ -2241,6 +3044,15 @@ "match": "(\\\\)(text(s(terling|ixoldstyle|urd|e(ction|venoldstyle|rvicemark))|yen|n(ineoldstyle|umero|aira)|c(ircledP|o(py(left|right)|lonmonetary)|urrency|e(nt(oldstyle)?|lsius))|t(hree(superior|oldstyle|quarters(emdash)?)|i(ldelow|mes)|w(o(superior|oldstyle)|elveudash)|rademark)|interrobang(down)?|zerooldstyle|o(hm|ne(superior|half|oldstyle|quarter)|penbullet|rd(feminine|masculine))|d(i(scount|ed|v(orced)?)|o(ng|wnarrow|llar(oldstyle)?)|egree|agger(dbl)?|blhyphen(char)?)|uparrow|p(ilcrow|e(so|r(t(housand|enthousand)|iodcentered))|aragraph|m)|e(stimated|ightoldstyle|uro)|quotes(traight(dblbase|base)|ingle)|f(iveoldstyle|ouroldstyle|lorin|ractionsolidus)|won|l(not|ira|e(ftarrow|af)|quill|angle|brackdbl)|a(s(cii(caron|dieresis|acute|grave|macron|breve)|teriskcentered)|cutedbl)|r(ightarrow|e(cipe|ferencemark|gistered)|quill|angle|brackdbl)|g(uarani|ravedbl)|m(ho|inus|u(sicalnote)?|arried)|b(igcircle|orn|ullet|lank|a(ht|rdbl)|rokenbar)))\\b", "name": "constant.character.latex" }, + { + "captures": { + "1": { + "name": "punctuation.definition.variable.latex" + } + }, + "match": "(\\\\)(?:[cgl]_+[_\\p{Alphabetic}@]+_[a-z]+|[qs]_[_\\p{Alphabetic}@]+[\\p{Alphabetic}@])", + "name": "variable.other.latex3.latex" + }, { "captures": { "1": { @@ -2258,92 +3070,34 @@ } ], "repository": { - "optional-arg": { - "patterns": [ - { - "captures": { - "1": { - "name": "punctuation.definition.arguments.optional.begin.latex" - }, - "2": { - "name": "variable.parameter.function.latex" - }, - "3": { - "name": "punctuation.definition.arguments.optional.end.latex" - } - }, - "match": "(\\[)([^\\[]*?)(\\])", - "name": "meta.parameter.optional.latex" - } - ] - }, - "multiline-optional-arg-no-highlight": { - "begin": "\\G\\[", - "beginCaptures": { - "0": { - "name": "punctuation.definition.arguments.optional.begin.latex" - } - }, - "end": "\\]", - "endCaptures": { - "0": { - "name": "punctuation.definition.arguments.optional.end.latex" - } - }, - "name": "meta.parameter.optional.latex", - "patterns": [ - { - "include": "$self" - } - ] - }, - "multiline-optional-arg": { - "begin": "\\G\\[", - "beginCaptures": { - "0": { - "name": "punctuation.definition.arguments.optional.begin.latex" - } - }, - "end": "\\]", - "endCaptures": { - "0": { - "name": "punctuation.definition.arguments.optional.end.latex" - } - }, - "name": "meta.parameter.optional.latex", - "contentName": "variable.parameter.function.latex", - "patterns": [ - { - "include": "$self" - } - ] - }, "autocites-arg": { "patterns": [ { "captures": { "1": { - "name": "punctuation.definition.arguments.optional.begin.latex" + "patterns": [ + { + "include": "#optional-arg-parenthesis-no-highlight" + } + ] }, "2": { - "name": "punctuation.definition.arguments.optional.end.latex" + "patterns": [ + { + "include": "#optional-arg-bracket-no-highlight" + } + ] }, "3": { - "name": "punctuation.definition.arguments.optional.begin.latex" - }, - "4": { - "name": "punctuation.definition.arguments.optional.end.latex" - }, - "5": { "name": "punctuation.definition.arguments.begin.latex" }, - "6": { + "4": { "name": "constant.other.reference.citation.latex" }, - "7": { + "5": { "name": "punctuation.definition.arguments.end.latex" }, - "8": { + "6": { "patterns": [ { "include": "#autocites-arg" @@ -2351,7 +3105,7 @@ ] } }, - "match": "(?:(\\()[^\\)]*(\\))){0,2}(?:(\\[)[^\\]]*(\\])){0,2}(\\{)([\\p{Alphabetic}\\p{Number}:.]+)(\\})(.*)" + "match": "((?:\\([^\\)]*\\)){0,2})((?:\\[[^\\]]*\\]){0,2})(\\{)([\\p{Alphabetic}\\p{Number}_:.-]+)(\\})(.*)" } ] }, @@ -2378,7 +3132,7 @@ "7": { "patterns": [ { - "include": "$base" + "include": "$self" } ] }, @@ -2395,10 +3149,10 @@ "name": "punctuation.definition.arguments.end.latex" } }, - "match": "\\s*((\\\\)(?:begin|end))(\\{)([a-zA-Z]*\\*?)(\\})(?:(\\[)(.*)(\\]))?(?:(\\{)([^{}]*)(\\}))?" + "match": "\\s*((\\\\)(?:begin|end))(\\{)([a-zA-Z]*\\*?)(\\})(?:(\\[)([^\\]]*)(\\])){,2}(?:(\\{)([^{}]*)(\\}))?" }, "definition-label": { - "begin": "((\\\\)label)((?:\\[[^\\[]*?\\])*)(\\{)", + "begin": "((\\\\)z?label)((?:\\[[^\\[]*?\\])*)(\\{)", "beginCaptures": { "1": { "name": "keyword.control.label.latex" @@ -2409,7 +3163,7 @@ "3": { "patterns": [ { - "include": "#optional-arg" + "include": "#optional-arg-bracket" } ] }, @@ -2430,6 +3184,133 @@ "name": "variable.parameter.definition.label.latex" } ] + }, + "multiline-optional-arg": { + "begin": "\\G\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.arguments.optional.begin.latex" + } + }, + "contentName": "variable.parameter.function.latex", + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "name": "meta.parameter.optional.latex", + "patterns": [ + { + "include": "$self" + } + ] + }, + "multiline-optional-arg-no-highlight": { + "begin": "\\G\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.arguments.optional.begin.latex" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "name": "meta.parameter.optional.latex", + "patterns": [ + { + "include": "$self" + } + ] + }, + "optional-arg-bracket": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(\\[)([^\\[]*?)(\\])", + "name": "meta.parameter.optional.latex" + } + ] + }, + "optional-arg-parenthesis": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "variable.parameter.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(\\()([^\\(]*?)(\\))", + "name": "meta.parameter.optional.latex" + } + ] + }, + "optional-arg-bracket-no-highlight": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(\\[)[^\\[]*?(\\])", + "name": "meta.parameter.optional.latex" + } + ] + }, + "optional-arg-angle-no-highlight": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(<)[^<]*?(>)", + "name": "meta.parameter.optional.latex" + } + ] + }, + "optional-arg-parenthesis-no-highlight": { + "patterns": [ + { + "captures": { + "1": { + "name": "punctuation.definition.arguments.optional.begin.latex" + }, + "2": { + "name": "punctuation.definition.arguments.optional.end.latex" + } + }, + "match": "(\\()[^\\(]*?(\\))", + "name": "meta.parameter.optional.latex" + } + ] } } } \ No newline at end of file diff --git a/extensions/latex/syntaxes/TeX.tmLanguage.json b/extensions/latex/syntaxes/TeX.tmLanguage.json index fd36f8691db..0cb03e61466 100644 --- a/extensions/latex/syntaxes/TeX.tmLanguage.json +++ b/extensions/latex/syntaxes/TeX.tmLanguage.json @@ -4,10 +4,39 @@ "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/jlelong/vscode-latex-basics/commit/69915f318570484ef40ed8798c73c63c58704183", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/5d7c2a4e451a932b776f6d9342087be6a1e8c0a1", "name": "TeX", "scopeName": "text.tex", "patterns": [ + { + "begin": "(?<=^\\s*)((\\\\)iffalse)", + "beginCaptures": { + "1": { + "name": "keyword.control.tex" + }, + "2": { + "name": "punctuation.definition.keyword.tex" + } + }, + "contentName": "comment.line.percentage.tex", + "end": "(?<=^\\s*)((\\\\)(?:else|fi))", + "endCaptures": { + "1": { + "name": "keyword.control.tex" + }, + "2": { + "name": "punctuation.definition.keyword.tex" + } + }, + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#conditionals" + } + ] + }, { "captures": { "1": { @@ -36,35 +65,7 @@ "name": "meta.catcode.tex" }, { - "begin": "(^[ \\t]+)?(?=%)", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.tex" - } - }, - "end": "(?!\\G)", - "patterns": [ - { - "begin": "%:?", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.tex" - } - }, - "end": "$\\n?", - "name": "comment.line.percentage.tex" - }, - { - "begin": "^(%!TEX) (\\S*) =", - "beginCaptures": { - "1": { - "name": "punctuation.definition.comment.tex" - } - }, - "end": "$\\n?", - "name": "comment.line.percentage.directive.tex" - } - ] + "include": "#comment" }, { "match": "[\\[\\]]", @@ -107,7 +108,25 @@ "name": "punctuation.definition.function.tex" } }, - "match": "(\\\\)(?:[\\p{Alphabetic}@]+|[,;])", + "match": "(\\\\)_*[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", + "name": "support.class.general.latex3.tex" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.function.tex" + } + }, + "match": "(\\.)[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", + "name": "support.class.general.latex3.tex" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.function.tex" + } + }, + "match": "(\\\\)(?:[,;]|(?:[\\p{Alphabetic}@]+))", "name": "support.function.general.tex" }, { @@ -121,6 +140,69 @@ } ], "repository": { + "braces": { + "begin": "(?|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -2150,14 +2133,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "12": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -2420,261 +2403,6 @@ }, "name": "meta.preprocessor.import.cpp" }, - "d9bc4796b0b_preprocessor_number_literal": { - "match": "(?|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -4261,14 +3972,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "24": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -4404,7 +4115,7 @@ "51": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -4527,24 +4238,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -4605,14 +4299,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "17": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -4686,7 +4380,7 @@ }, "function_pointer": { "begin": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:unsigned)|(?:signed)|(?:short)|(?:long))|(?:(?:struct)|(?:class)|(?:union)|(?:enum)))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", - "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?=[{=,);>]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", + "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { "name": "meta.qualified_type.cpp", @@ -4718,24 +4412,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -4796,14 +4473,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "12": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -4955,6 +4632,41 @@ }, "5": { "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "6": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "7": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "8": { + "name": "comment.block.cpp" + }, + "9": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "10": { + "name": "storage.modifier.specifier.functional.post-parameters.$10.cpp" + }, + "11": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "12": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "13": { + "name": "comment.block.cpp" + }, + "14": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" } }, "patterns": [ @@ -4965,7 +4677,7 @@ }, "function_pointer_parameter": { "begin": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:unsigned)|(?:signed)|(?:short)|(?:long))|(?:(?:struct)|(?:class)|(?:union)|(?:enum)))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", - "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?=[{=,);>]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", + "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { "name": "meta.qualified_type.cpp", @@ -4997,24 +4709,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -5075,14 +4770,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "12": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -5234,6 +4929,41 @@ }, "5": { "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "6": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "7": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "8": { + "name": "comment.block.cpp" + }, + "9": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "10": { + "name": "storage.modifier.specifier.functional.post-parameters.$10.cpp" + }, + "11": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "12": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "13": { + "name": "comment.block.cpp" + }, + "14": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" } }, "patterns": [ @@ -5613,24 +5343,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -5709,14 +5422,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "8": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -5787,7 +5500,7 @@ "name": "invalid.illegal.unexpected.punctuation.definition.comment.end.cpp" }, "label": { - "match": "((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -6872,14 +6568,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "24": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -7107,24 +6803,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -7185,14 +6864,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "24": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -7655,24 +7334,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -7733,14 +7395,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "16": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -7890,14 +7552,14 @@ "name": "entity.name.scope-resolution.operator.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "46": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -7951,14 +7613,14 @@ "name": "entity.name.scope-resolution.operator-overload.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "59": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -8111,7 +7773,7 @@ "include": "#ever_present_context" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" }, { "begin": "\\(", @@ -9611,7 +9273,7 @@ "include": "#scope_resolution_parameter_inner_generated" }, { - "match": "(?:(?:struct)|(?:class)|(?:union)|(?:enum))", + "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.])", "captures": { @@ -11416,24 +11333,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -11512,14 +11412,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "7": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -11659,24 +11559,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -11737,14 +11620,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "21": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -11940,24 +11823,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -12018,14 +11884,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "21": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12249,7 +12115,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12271,7 +12137,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12293,7 +12159,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12305,7 +12171,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12340,7 +12206,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12362,7 +12228,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12374,7 +12240,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12409,7 +12275,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12431,7 +12297,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12443,7 +12309,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12478,7 +12344,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12490,7 +12356,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12525,7 +12391,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12547,7 +12413,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12559,7 +12425,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12594,7 +12460,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12616,7 +12482,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12628,7 +12494,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12663,7 +12529,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12685,7 +12551,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12697,7 +12563,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12732,7 +12598,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12754,7 +12620,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12766,7 +12632,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12801,7 +12667,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12823,7 +12689,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12835,7 +12701,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12870,7 +12736,7 @@ "2": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] } @@ -12892,7 +12758,7 @@ "3": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12904,7 +12770,7 @@ "name": "meta.template.call.cpp", "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -12960,24 +12826,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -13056,14 +12905,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "8": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -14310,8 +14159,8 @@ ] }, { - "begin": "((?:[uUL]8?)?R)\\\"(?:[pP]?(?:sql|SQL)|d[dm]l)\\(", - "end": "\\)(?:[pP]?(?:sql|SQL)|d[dm]l)\\\"|(?=\\\\end\\{(?:minted|cppcode)\\})", + "begin": "((?:[uUL]8?)?R)\\\"\\(", + "end": "\\)\\\"|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.cpp" @@ -14325,18 +14174,16 @@ "name": "punctuation.definition.string.end.cpp" } }, - "name": "meta.string.quoted.double.raw.sql.cpp", + "name": "string.quoted.double.raw.cpp", "patterns": [ - { - "include": "source.sql" - } + {} ] }, { "begin": "((?:u|u8|U|L)?R)\"(?:([^ ()\\\\\\t]{0,16})|([^ ()\\\\\\t]*))\\(", "beginCaptures": { "0": { - "name": "punctuation.definition.string.begin" + "name": "punctuation.definition.string.$2.begin" }, "1": { "name": "meta.encoding" @@ -14345,25 +14192,29 @@ "name": "invalid.illegal.delimiter-too-long" } }, - "end": "(\\)\\2(\\3)\")(?:((?:[a-zA-Z]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)|(_(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*))?|(?=\\\\end\\{(?:minted|cppcode)\\})", + "end": "(\\)(\\2)(\\3)\")(?:((?:[a-zA-Z]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)|(_(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*))?|(?=\\\\end\\{(?:minted|cppcode)\\})", "endCaptures": { "1": { - "name": "punctuation.definition.string.end" - }, - "2": { - "name": "invalid.illegal.delimiter-too-long" + "name": "punctuation.definition.string.$2.end" }, "3": { - "name": "keyword.other.suffix.literal.user-defined.reserved.string.cpp" + "name": "invalid.illegal.delimiter-too-long" }, "4": { + "name": "keyword.other.suffix.literal.user-defined.reserved.string.cpp" + }, + "5": { "name": "keyword.other.suffix.literal.user-defined.string.cpp" } }, - "name": "string.quoted.double.raw" + "name": "string.quoted.double.raw.$2" } ] }, + "string_escaped_char": { + "match": "(?x)\\\\ (\n\\\\\t\t\t |\n[abefnprtv'\"?] |\n[0-3][0-7]{,2}\t |\n[4-7]\\d?\t\t|\nx[a-fA-F0-9]{,2} |\nu[a-fA-F0-9]{,4} |\nU[a-fA-F0-9]{,8} )", + "name": "constant.character.escape" + }, "string_escapes_context_c": { "patterns": [ { @@ -14575,7 +14426,7 @@ "include": "#inheritance_context" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -14947,7 +14798,7 @@ "include": "#ever_present_context" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" }, { "include": "#storage_types" @@ -14981,7 +14832,7 @@ "0": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -15017,6 +14868,615 @@ } ] }, + "template_call_range_helper": { + "patterns": [ + { + "match": "\\b((?|(?=\\\\end\\{(?:minted|cppcode)\\})", @@ -15400,24 +15860,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -15496,14 +15939,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "15": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -15906,7 +16349,7 @@ "include": "#inheritance_context" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -16065,7 +16508,7 @@ "patterns": [ { "begin": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:unsigned)|(?:signed)|(?:short)|(?:long))|(?:(?:struct)|(?:class)|(?:union)|(?:enum)))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", - "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?=[{=,);>]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", + "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { "name": "meta.qualified_type.cpp", @@ -16097,24 +16540,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -16175,14 +16601,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "12": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -16334,6 +16760,41 @@ }, "5": { "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "6": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "7": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "8": { + "name": "comment.block.cpp" + }, + "9": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "10": { + "name": "storage.modifier.specifier.functional.post-parameters.$10.cpp" + }, + "11": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "12": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "13": { + "name": "comment.block.cpp" + }, + "14": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" } }, "patterns": [ @@ -16549,7 +17010,7 @@ "include": "#inheritance_context" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -16901,7 +17362,7 @@ "include": "#inheritance_context" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -17173,24 +17634,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -17269,14 +17713,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "13": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -17569,7 +18013,7 @@ "include": "#inheritance_context" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -17828,7 +18272,7 @@ "5": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -17931,24 +18375,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -18027,14 +18454,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "12": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, @@ -18296,24 +18723,7 @@ "include": "#scope_resolution_inner_generated" }, { - "begin": "<", - "end": ">|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.begin.template.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.angle-brackets.end.template.call.cpp" - } - }, - "name": "meta.template.call.cpp", - "patterns": [ - { - "include": "#template_call_context" - } - ] + "include": "#template_call_range_helper" }, { "match": "(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*", @@ -18392,14 +18802,14 @@ "name": "entity.name.scope-resolution.type.cpp" }, { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, "12": { "patterns": [ { - "include": "#template_call_range" + "include": "#template_call_range_helper" } ] }, diff --git a/extensions/latex/syntaxes/markdown-latex-combined.tmLanguage.json b/extensions/latex/syntaxes/markdown-latex-combined.tmLanguage.json index a034ea4ff01..75c6c2da05a 100644 --- a/extensions/latex/syntaxes/markdown-latex-combined.tmLanguage.json +++ b/extensions/latex/syntaxes/markdown-latex-combined.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/jlelong/vscode-latex-basics/commit/45c7b12ee68563afd50407e5eac02d30d33dbe7a", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/56e2dc967e6bafafc1acfeeb80af42b8328b021a", "name": "Markdown", "scopeName": "text.tex.markdown_latex_combined", "patterns": [ @@ -1260,7 +1260,7 @@ ] }, "fenced_code_block_powershell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|,|\\{|\\?)[^`]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1|pwsh)((\\s+|:|,|\\{|\\?)[^`]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { diff --git a/extensions/less/cgmanifest.json b/extensions/less/cgmanifest.json index 663e1cc4daf..69a66b5d9b5 100644 --- a/extensions/less/cgmanifest.json +++ b/extensions/less/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-less", "repositoryUrl": "https://github.com/radium-v/Better-Less", - "commitHash": "05de79f600227201e35f07a49f07acce80e49dbf" + "commitHash": "fb9c21917193746433743a7c971b70230b40bc2b" } }, "license": "MIT", diff --git a/extensions/less/syntaxes/less.tmLanguage.json b/extensions/less/syntaxes/less.tmLanguage.json index 3d2c6bdaeb0..6f57a48e02d 100644 --- a/extensions/less/syntaxes/less.tmLanguage.json +++ b/extensions/less/syntaxes/less.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/radium-v/Better-Less/commit/05de79f600227201e35f07a49f07acce80e49dbf", + "version": "https://github.com/radium-v/Better-Less/commit/fb9c21917193746433743a7c971b70230b40bc2b", "name": "Less", "scopeName": "source.css.less", "patterns": [ @@ -40,6 +40,14 @@ "match": "(?i:[-+]?(?:(?:\\d*\\.\\d+(?:[eE](?:[-+]?\\d+))*)|(?:[-+]?\\d+))(deg|grad|rad|turn))\\b", "name": "constant.numeric.less" }, + "arbitrary-repetition": { + "captures": { + "1": { + "name": "punctuation.definition.arbitrary-repetition.less" + } + }, + "match": "\\s*(?:(,))" + }, "at-charset": { "begin": "\\s*((@)charset\\b)\\s*", "captures": { @@ -268,6 +276,9 @@ "patterns": [ { "include": "#keyframe-name" + }, + { + "include": "#arbitrary-repetition" } ] } @@ -615,6 +626,9 @@ { "include": "#filter-function" }, + { + "include": "#fit-content-function" + }, { "include": "#format-function" }, @@ -697,6 +711,9 @@ }, { "include": "#less-math" + }, + { + "include": "#relative-color" } ] } @@ -715,6 +732,7 @@ "name": "support.function.color.less" } }, + "comment": "rgb(), rgba()", "end": "\\)", "endCaptures": { "0": { @@ -738,9 +756,15 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#comma-delimiter" }, + { + "include": "#value-separator" + }, { "include": "#percentage-type" }, @@ -752,12 +776,13 @@ ] }, { - "begin": "\\b(hs(l|v)a?|hwb)(?=\\()", + "begin": "\\b(hsla|hsl|hwb|oklab|oklch|lab|lch)(?=\\()", "beginCaptures": { "1": { "name": "support.function.color.less" } }, + "comment": "hsla, hsl, hwb, oklab, oklch, lab, lch", "end": "\\)", "endCaptures": { "0": { @@ -775,12 +800,18 @@ }, "end": "(?=\\))", "patterns": [ + { + "include": "#color-values" + }, { "include": "#less-strings" }, { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#comma-delimiter" }, @@ -792,6 +823,47 @@ }, { "include": "#number-type" + }, + { + "include": "#calc-function" + }, + { + "include": "#value-separator" + } + ] + } + ] + }, + { + "begin": "\\b(light-dark)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.color.less" + } + }, + "comment": "light-dark()", + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#color-values" + }, + { + "include": "#comma-delimiter" } ] } @@ -813,6 +885,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "match": "\\b(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)\\b", "name": "support.constant.color.w3c-standard-color-name.less" @@ -833,6 +908,9 @@ }, "match": "(#)(\\h{3}|\\h{4}|\\h{6}|\\h{8})\\b", "name": "constant.other.color.rgb-value.less" + }, + { + "include": "#relative-color" } ] }, @@ -902,6 +980,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "match": "(?:--(?:[[-\\w][^\\x{00}-\\x{7F}]]|(?:\\\\\\h{1,6}[\\s\\t\\n\\f]?|\\\\[^\\n\\f\\h]))+|-?(?:[[_a-zA-Z][^\\x{00}-\\x{7F}]]|(?:\\\\\\h{1,6}[\\s\\t\\n\\f]?|\\\\[^\\n\\f\\h]))(?:[[-\\w][^\\x{00}-\\x{7F}]]|(?:\\\\\\h{1,6}[\\s\\t\\n\\f]?|\\\\[^\\n\\f\\h]))*)", "name": "entity.other.counter-name.less" @@ -961,6 +1042,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#literal-string" }, @@ -1030,12 +1114,16 @@ ] }, "cubic-bezier-function": { - "begin": "\\b(cubic-bezier)(?=\\()", + "begin": "\\b(cubic-bezier)(\\()", "beginCaptures": { - "0": { + "1": { "name": "support.function.timing.less" + }, + "2": { + "name": "punctuation.definition.group.begin.less" } }, + "contentName": "meta.group.less", "end": "\\)", "endCaptures": { "0": { @@ -1045,21 +1133,22 @@ "name": "meta.function-call.less", "patterns": [ { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.group.begin.less" - } - }, - "end": "(?=\\))", - "patterns": [ - { - "include": "#comma-delimiter" - }, - { - "include": "#number-type" - } - ] + "include": "#less-functions" + }, + { + "include": "#calc-function" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#comma-delimiter" + }, + { + "include": "#number-type" } ] }, @@ -1083,14 +1172,14 @@ { "include": "#frequency-type" }, + { + "include": "#time-type" + }, { "include": "#length-type" }, { "include": "#resolution-type" - }, - { - "include": "#time-type" } ] }, @@ -1275,6 +1364,49 @@ } ] }, + "fit-content-function": { + "begin": "\\b(fit-content)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.grid.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" + }, + { + "include": "#length-type" + }, + { + "include": "#percentage-type" + } + ] + } + ] + }, "format-function": { "patterns": [ { @@ -1348,6 +1480,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#angle-type" }, @@ -1402,6 +1537,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#color-values" }, @@ -1541,6 +1679,15 @@ } ] }, + "important": { + "captures": { + "1": { + "name": "punctuation.separator.less" + } + }, + "match": "(\\!)\\s*important", + "name": "keyword.other.important.less" + }, "integer-type": { "match": "(?:[-+]?\\d+)", "name": "constant.numeric.less" @@ -1628,6 +1775,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#comma-delimiter" }, @@ -1684,6 +1834,7 @@ "name": "support.function.color-definition.less" } }, + "comment": "argb()", "end": "\\)", "endCaptures": { "0": { @@ -1704,12 +1855,68 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#color-values" } ] } ] + }, + { + "begin": "\\b(hsva?)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.color.less" + } + }, + "comment": "hsva(), hsv()", + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#integer-type" + }, + { + "include": "#percentage-type" + }, + { + "include": "#number-type" + }, + { + "include": "#less-strings" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" + }, + { + "include": "#comma-delimiter" + } + ] + } + ] } ] }, @@ -3360,6 +3567,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#length-type" }, @@ -3421,7 +3631,63 @@ "property-value-constants": { "patterns": [ { - "match": "(?x)\\b(\n absolute|active|add\n |all(-(petite|small)-caps|-scroll)?\n |alpha(betic)?\n |alternate(-reverse)?\n |always|annotation|antialiased|at\n |auto(hiding-scrollbar)?\n |avoid(-column|-page|-region)?\n |background(-color|-image|-position|-size)?\n |backwards|balance|baseline|below|bevel|bicubic|bidi-override|blink\n |block(-line-height)?\n |blur\n |bold(er)?\n |border(-bottom|-left|-right|-top)?-(color|radius|width|style)\n |border-(bottom|top)-(left|right)-radius\n |border-image(-outset|-repeat|-slice|-source|-width)?\n |border(-bottom|-left|-right|-top|-collapse|-spacing|-box)?\n |both|bottom\n |box(-shadow)?\n |break-(all|word)\n |brightness\n |butt(on)?\n |capitalize\n |cent(er|ral)\n |char(acter-variant)?\n |cjk-ideographic|clip|clone|close-quote\n |closest-(corner|side)\n |col-resize|collapse\n |color(-stop|-burn|-dodge)?\n |column((-count|-gap|-reverse|-rule(-color|-width)?|-width)|s)?\n |common-ligatures|condensed|consider-shifts|contain\n |content(-box|s)?\n |contextual|contrast|cover\n |crisp(-e|E)dges\n |crop\n |cross(hair)?\n |da(rken|shed)\n |default|dense|diagonal-fractions|difference|disabled\n |discretionary-ligatures|disregard-shifts\n |distribute(-all-lines|-letter|-space)?\n |dotted|double|drop-shadow\n |(nwse|nesw|ns|ew|sw|se|nw|ne|w|s|e|n)-resize\n |ease(-in-out|-in|-out)?\n |element|ellipsis|embed|end|EndColorStr|evenodd\n |exclu(de(-ruby)?|sion)\n |expanded\n |(extra|semi|ultra)-(condensed|expanded)\n |farthest-(corner|side)?\n |fill(-box|-opacity)?\n |filter|fixed|flat\n |flex((-basis|-end|-grow|-shrink|-start)|box)?\n |flip|flood-color\n |font(-size(-adjust)?|-stretch|-weight)?\n |forwards\n |from(-image)?\n |full-width|geometricPrecision|glyphs|gradient|grayscale\n |grid(-height)?\n |groove|hand|hanging|hard-light|height|help|hidden|hide\n |historical-(forms|ligatures)\n |horizontal(-tb)?\n |hue\n |ideograph(-alpha|-numeric|-parenthesis|-space|ic)\n |inactive|include-ruby|infinite|inherit|initial\n |inline(-block|-box|-flex(box)?|-line-height|-table)?\n |inset|inside\n |inter(-ideograph|-word|sect)\n |invert|isolat(e|ion)|italic\n |jis(04|78|83|90)\n |justify(-all)?\n |keep-all\n |large[r]?\n |last|layout|left|letter-spacing\n |light(e[nr]|ing-color)\n |line(-edge|-height|-through)?\n |linear(-gradient|RGB)?\n |lining-nums|list-item|local|loose|lowercase|lr-tb|ltr\n |lumin(osity|ance)|manual\n |manipulation\n |margin(-bottom|-box|-left|-right|-top)?\n |marker(-offset|s)?\n |mathematical\n |max-(content|height|lines|size|width)\n |medium|middle\n |min-(content|height|width)\n |miter|mixed|move|multiply|newspaper\n |no-(change|clip|(close|open)-quote|(common|discretionary|historical)-ligatures|contextual|drop|repeat)\n |none|nonzero|normal|not-allowed|nowrap|oblique\n |offset(-after|-before|-end|-start)?\n |oldstyle-nums|opacity|open-quote\n |optimize(Legibility|Precision|Quality|Speed)\n |order|ordinal|ornaments\n |outline(-color|-offset|-width)?\n |outset|outside|over(line|-edge|lay)\n |padding(-bottom|-box|-left|-right|-top|-box)?\n |page|painted|paused\n |pan-(x|left|right|y|up|down)\n |perspective-origin\n |petite-caps|pixelated|pointer\n |pinch-zoom\n |pre(-line|-wrap)?\n |preserve-3d\n |progid:DXImageTransform.Microsoft.(Alpha|Blur|dropshadow|gradient|Shadow)\n |progress\n |proportional-(nums|width)\n |radial-gradient|recto|region|relative\n |repeat(-[xy])?\n |repeating-(linear|radial)-gradient\n |replaced|reset-size|reverse|ridge|right\n |round\n |row(-resize|-reverse)?\n |rtl|ruby|running|saturat(e|ion)|screen\n |scroll(-position|bar)?\n |separate|sepia\n |scale-down\n |shape-(image-threshold|margin|outside)\n |show\n |sideways(-lr|-rl)?\n |simplified\n |size\n |slashed-zero|slice\n |small(-caps|er)?\n |smooth|snap|solid|soft-light\n |space(-around|-between)?\n |span|sRGB\n |stack(ed-fractions)?\n |start(ColorStr)?\n |static\n |step-(end|start)\n |sticky\n |stop-(color|opacity)\n |stretch|strict\n |stroke(-box|-dash(array|offset)|-miterlimit|-opacity|-width)?\n |style(set)?\n |stylistic\n |sub(grid|pixel-antialiased|tract)?\n |super|swash\n |table(-caption|-cell|(-column|-footer|-header|-row)-group|-column|-row)?\n |tabular-nums|tb-rl\n |text((-bottom|-(decoration|emphasis)-color|-indent|-(over|under)-edge|-shadow|-size(-adjust)?|-top)|field)?\n |thi(ck|n)\n |titling-ca(ps|se)\n |to[p]?\n |touch|traditional\n |transform(-origin)?\n |under(-edge|line)?\n |unicase|unset|uppercase|upright\n |use-(glyph-orientation|script)\n |verso\n |vertical(-align|-ideographic|-lr|-rl|-text)?\n |view-box\n |viewport-fill(-opacity)?\n |visibility\n |visible(Fill|Painted|Stroke)?\n |wait|wavy|weight|whitespace|(device-)?width|word-spacing\n |wrap(-reverse)?\n |x{1,2}-(large|small)\n |z-index|zero\n |zoom(-in|-out)?\n |((?xi:arabic-indic|armenian|bengali|cambodian|circle|cjk-decimal|cjk-earthly-branch|cjk-heavenly-stem|decimal-leading-zero|decimal|devanagari|disclosure-closed|disclosure-open|disc|ethiopic-numeric|georgian|gujarati|gurmukhi|hebrew|hiragana-iroha|hiragana|japanese-formal|japanese-informal|kannada|katakana-iroha|katakana|khmer|korean-hangul-formal|korean-hanja-formal|korean-hanja-informal|lao|lower-alpha|lower-armenian|lower-greek|lower-latin|lower-roman|malayalam|mongolian|myanmar|oriya|persian|simp-chinese-formal|simp-chinese-informal|square|tamil|telugu|thai|tibetan|trad-chinese-formal|trad-chinese-informal|upper-alpha|upper-armenian|upper-latin|upper-roman)))\\b", + "comment": "align-content, align-items, align-self, justify-content, justify-items, justify-self", + "match": "(?x)\\b(?:\n flex-start|flex-end|start|end|space-between|space-around|space-evenly\n |stretch|baseline|safe|unsafe|legacy|anchor-center|first|last|self-start|self-end\n)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "alignment-baseline", + "match": "(?x)\\b(?:\n text-before-edge|before-edge|middle|central|text-after-edge\n |after-edge|ideographic|alphabetic|hanging|mathematical|top|center|bottom\n)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "all/global values", + "match": "\\b(?:initial|inherit|unset|revert-layer|revert)\\b", + "name": "support.constant.property-value.less" + }, + { + "include": "#cubic-bezier-function" + }, + { + "include": "#steps-function" + }, + { + "comment": "animation-composition", + "match": "\\b(?:replace|add|accumulate)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-direction", + "match": "\\b(?:normal|alternate-reverse|alternate|reverse)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-fill-mode", + "match": "\\b(?:forwards|backwards|both)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-iteration-count", + "match": "\\b(?:infinite)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-play-state", + "match": "\\b(?:running|paused)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-range, animation-range-start, animation-range-end", + "match": "\\b(?:entry-crossing|exit-crossing|entry|exit)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "animation-timing-function", + "match": "\\b(?:linear|ease-in-out|ease-in|ease-out|ease|step-start|step-end)\\b", + "name": "support.constant.property-value.less" + }, + { + "match": "(?x)\\b(\n absolute|active|add\n|all(-(petite|small)-caps|-scroll)?\n|alpha(betic)?\n|alternate(-reverse)?\n|always|annotation|antialiased|at\n|auto(hiding-scrollbar)?\n|avoid(-column|-page|-region)?\n|background(-color|-image|-position|-size)?\n|backwards|balance|baseline|below|bevel|bicubic|bidi-override|blink\n|block(-(line-height|start|end))?\n|blur\n|bold(er)?\n|border-top-left-radius\n|border-top-right-radius\n|border-bottom-left-radius\n|border-bottom-right-radius\n|border-end-end-radius\n|border-end-start-radius\n|border-start-end-radius\n|border-start-start-radius\n|border-block-start-color\n|border-block-start-style\n|border-block-start-width\n|border-block-start\n|border-block-end-color\n|border-block-end-style\n|border-block-end-width\n|border-block-end\n|border-block-color\n|border-block-style\n|border-block-width\n|border-block\n|border-inline-start-color\n|border-inline-start-style\n|border-inline-start-width\n|border-inline-start\n|border-inline-end-color\n|border-inline-end-style\n|border-inline-end-width\n|border-inline-end\n|border-inline-color\n|border-inline-style\n|border-inline-width\n|border-inline\n|border-top-color\n|border-top-style\n|border-top-width\n|border-top\n|border-right-color\n|border-right-style\n|border-right-width\n|border-right\n|border-bottom-color\n|border-bottom-style\n|border-bottom-width\n|border-bottom\n|border-left-color\n|border-left-style\n|border-left-width\n|border-left\n|border-image-outset\n|border-image-repeat\n|border-image-slice\n|border-image-source\n|border-image-width\n|border-image\n|border-color\n|border-style\n|border-width\n|border-radius\n|border-collapse\n|border-spacing\n|border\n|both\n|bottom\n|box(-shadow)?\n|break-(all|word|spaces)\n|brightness\n|butt(on)?\n|capitalize\n|cent(er|ral)\n|char(acter-variant)?\n|cjk-ideographic|clip|clone|close-quote\n|closest-(corner|side)\n|col-resize|collapse\n|color(-stop|-burn|-dodge)?\n|column((-count|-gap|-reverse|-rule(-color|-width)?|-width)|s)?\n|common-ligatures|condensed|consider-shifts|contain\n|content(-box|s)?\n|contextual|contrast|cover\n|crisp(-e|E)dges\n|crop\n|cross(hair)?\n|da(rken|shed)\n|default|dense|diagonal-fractions|difference|disabled\n|discard|discretionary-ligatures|disregard-shifts\n|distribute(-all-lines|-letter|-space)?\n|dotted|double|drop-shadow\n|(nwse|nesw|ns|ew|sw|se|nw|ne|w|s|e|n)-resize\n|ease(-in-out|-in|-out)?\n|element|ellipsis|embed|end|EndColorStr|evenodd\n|exclu(de(-ruby)?|sion)\n|expanded\n|(extra|semi|ultra)-(condensed|expanded)\n|farthest-(corner|side)?\n|fill(-box|-opacity)?\n|filter\n|fit-content\n|fixed\n|flat\n|flex((-basis|-end|-grow|-shrink|-start)|box)?\n|flip|flood-color\n|font(-size(-adjust)?|-stretch|-weight)?\n|forwards\n|from(-image)?\n|full-width|gap|geometricPrecision|glyphs|gradient|grayscale\n|grid((-column|-row)?-gap|-height)?\n|groove|hand|hanging|hard-light|height|help|hidden|hide\n|historical-(forms|ligatures)\n|horizontal(-tb)?\n|hue\n|ideograph(-alpha|-numeric|-parenthesis|-space|ic)\n|inactive|include-ruby|infinite|inherit|initial\n|inline(-(block|box|flex(box)?|line-height|table|start|end))?\n|inset|inside\n|inter(-ideograph|-word|sect)\n|invert|isolat(e|ion)|italic\n|jis(04|78|83|90)\n|justify(-all)?\n|keep-all\n|large[r]?\n|last|layout|left|letter-spacing\n|light(e[nr]|ing-color)\n|line(-edge|-height|-through)?\n|linear(-gradient|RGB)?\n|lining-nums|list-item|local|loose|lowercase|lr-tb|ltr\n|lumin(osity|ance)|manual\n|manipulation\n|margin(-bottom|-box|-left|-right|-top)?\n|marker(-offset|s)?\n|match-parent\n|mathematical\n|max-(content|height|lines|size|width)\n|medium|middle\n|min-(content|height|width)\n|miter|mixed|move|multiply|newspaper\n|no-(change|clip|(close|open)-quote|(common|discretionary|historical)-ligatures|contextual|drop|repeat)\n|none|nonzero|normal|not-allowed|nowrap|oblique\n|offset(-after|-before|-end|-start)?\n|oldstyle-nums|opacity|open-quote\n|optimize(Legibility|Precision|Quality|Speed)\n|order|ordinal|ornaments\n|outline(-color|-offset|-width)?\n|outset|outside|over(line|-edge|lay)\n|padding(-bottom|-box|-left|-right|-top|-box)?\n|page|paint(ed)?|paused\n|pan-(x|left|right|y|up|down)\n|perspective-origin\n|petite-caps|pixelated|pointer\n|pinch-zoom\n|pretty\n|pre(-line|-wrap)?\n|preserve(-3d|-breaks|-spaces)?\n|progid:DXImageTransform.Microsoft.(Alpha|Blur|dropshadow|gradient|Shadow)\n|progress\n|proportional-(nums|width)\n|radial-gradient|recto|region|relative\n|repeat(-[xy])?\n|repeating-(linear|radial)-gradient\n|replaced|reset-size|reverse|revert(-layer)?|ridge|right\n|round\n|row(-gap|-resize|-reverse)?\n|rtl|ruby|running|saturat(e|ion)|screen\n|scroll(-position|bar)?\n|separate|sepia\n|scale-down\n|shape-(image-threshold|margin|outside)\n|show\n|sideways(-lr|-rl)?\n|simplified\n|size\n|slashed-zero|slice\n|small(-caps|er)?\n|smooth|snap|solid|soft-light\n|space(-around|-between)?\n|span|sRGB\n|stable\n|stack(ed-fractions)?\n|start(ColorStr)?\n|static\n|step-(end|start)\n|sticky\n|stop-(color|opacity)\n|stretch|strict\n|stroke(-box|-dash(array|offset)|-miterlimit|-opacity|-width)?\n|style(set)?\n|stylistic\n|sub(grid|pixel-antialiased|tract)?\n|super|swash\n|table(-caption|-cell|(-column|-footer|-header|-row)-group|-column|-row)?\n|tabular-nums|tb-rl\n|text((-bottom|-(decoration|emphasis)-color|-indent|-(over|under)-edge|-shadow|-size(-adjust)?|-top)|field)?\n|thi(ck|n)\n|titling-ca(ps|se)\n|to[p]?\n|touch|traditional\n|transform(-origin)?\n|under(-edge|line)?\n|unicase|unset|uppercase|upright\n|use-(glyph-orientation|script)\n|verso\n|vertical(-align|-ideographic|-lr|-rl|-text)?\n|view-box\n|viewport-fill(-opacity)?\n|visibility\n|visible(Fill|Painted|Stroke)?\n|wait|wavy|weight|whitespace|(device-)?width|word-spacing\n|wrap(-reverse)?\n|x{1,2}-(large|small)\n|z-index|zero\n|zoom(-in|-out)?\n|((?xi:arabic-indic|armenian|bengali|cambodian|circle|cjk-decimal|cjk-earthly-branch|cjk-heavenly-stem|decimal-leading-zero|decimal|devanagari|disclosure-closed|disclosure-open|disc|ethiopic-numeric|georgian|gujarati|gurmukhi|hebrew|hiragana-iroha|hiragana|japanese-formal|japanese-informal|kannada|katakana-iroha|katakana|khmer|korean-hangul-formal|korean-hanja-formal|korean-hanja-informal|lao|lower-alpha|lower-armenian|lower-greek|lower-latin|lower-roman|malayalam|mongolian|myanmar|oriya|persian|simp-chinese-formal|simp-chinese-informal|square|tamil|telugu|thai|tibetan|trad-chinese-formal|trad-chinese-informal|upper-alpha|upper-armenian|upper-latin|upper-roman)))\\b", "name": "support.constant.property-value.less" }, { @@ -3444,9 +3710,6 @@ { "include": "#color-functions" }, - { - "include": "#less-math" - }, { "include": "#less-functions" }, @@ -3465,17 +3728,17 @@ { "include": "#property-value-constants" }, + { + "include": "#less-math" + }, { "include": "#literal-string" }, { - "captures": { - "1": { - "name": "punctuation.separator.less" - } - }, - "match": "(\\!)\\s*important", - "name": "keyword.other.important.less" + "include": "#comma-delimiter" + }, + { + "include": "#important" } ] }, @@ -3733,6 +3996,18 @@ } ] }, + "relative-color": { + "patterns": [ + { + "match": "from", + "name": "keyword.other.less" + }, + { + "match": "\\b[hslawbch]\\b", + "name": "keyword.other.less" + } + ] + }, "resolution-type": { "captures": { "1": { @@ -3802,6 +4077,41 @@ { "include": "#filter-function" }, + { + "begin": "\\b(border((-(bottom|top)-(left|right))|((-(start|end)){2}))?-radius|(border-image(?!-)))\\b", + "beginCaptures": { + "0": { + "name": "support.type.property-name.less" + } + }, + "comment": "border-radius and border-image properties utilize a slash as a separator", + "end": "\\s*(;)|(?=[})])", + "endCaptures": { + "1": { + "name": "punctuation.terminator.rule.less" + } + }, + "patterns": [ + { + "begin": "(((\\+_?)?):)(?=[\\s\\t]*)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.less" + } + }, + "contentName": "meta.property-value.less", + "end": "(?=\\s*(;)|(?=[})]))", + "patterns": [ + { + "include": "#value-separator" + }, + { + "include": "#property-values" + } + ] + } + ] + }, { "captures": { "1": { @@ -3854,7 +4164,7 @@ ] }, { - "begin": "\\banimation(-(delay|direction|duration|fill-mode|iteration-count|name|play-state|timing-function))?\\b", + "begin": "\\banimation-timeline\\b", "beginCaptures": { "0": { "name": "support.type.property-name.less" @@ -3874,42 +4184,91 @@ "name": "punctuation.separator.key-value.less" } }, - "captures": { + "contentName": "meta.property-value.less", + "end": "(?=\\s*(;)|(?=[})]))", + "patterns": [ + { + "include": "#comment-block" + }, + { + "include": "#custom-property-name" + }, + { + "include": "#scroll-function" + }, + { + "include": "#view-function" + }, + { + "include": "#property-values" + }, + { + "include": "#less-variables" + }, + { + "include": "#arbitrary-repetition" + }, + { + "include": "#important" + } + ] + } + ] + }, + { + "begin": "\\banimation(?:-name)?(?=(?:\\+_?)?:)\\b", + "beginCaptures": { + "0": { + "name": "support.type.property-name.less" + } + }, + "end": "\\s*(;)|(?=[})])", + "endCaptures": { + "1": { + "name": "punctuation.terminator.rule.less" + } + }, + "patterns": [ + { + "begin": "(((\\+_?)?):)(?=[\\s\\t]*)", + "beginCaptures": { "1": { - "name": "punctuation.definition.arbitrary-repetition.less" + "name": "punctuation.separator.key-value.less" } }, "contentName": "meta.property-value.less", "end": "(?=\\s*(;)|(?=[})]))", "patterns": [ { - "match": "\\b(linear|ease(-in)?(-out)?|step-(start|end)|none|forwards|backwards|both|normal|alternate(-reverse)?|reverse|running|paused)\\b", - "name": "support.constant.property-value.less" + "include": "#comment-block" }, { - "include": "#cubic-bezier-function" + "include": "#builtin-functions" }, { - "include": "#steps-function" + "include": "#less-functions" }, { - "include": "#time-type" + "include": "#less-variables" }, { - "include": "#number-type" + "include": "#numeric-values" + }, + { + "include": "#property-value-constants" }, { "match": "-?(?:[_a-zA-Z]|[^\\x{00}-\\x{7F}]|(?:(:?\\\\[0-9a-f]{1,6}(\\r\\n|[\\s\\t\\r\\n\\f])?)|\\\\[^\\r\\n\\f0-9a-f]))(?:[-_a-zA-Z0-9]|[^\\x{00}-\\x{7F}]|(?:(:?\\\\[0-9a-f]{1,6}(\\r\\n|[\\t\\r\\n\\f])?)|\\\\[^\\r\\n\\f0-9a-f]))*", - "name": "variable.other.constant.animation-name.less" + "name": "variable.other.constant.animation-name.less string.unquoted.less" }, { - "include": "#literal-string" + "include": "#less-math" }, { - "include": "#property-values" + "include": "#arbitrary-repetition" }, { - "match": "\\s*(?:(,))" + "include": "#important" } ] } @@ -3918,9 +4277,6 @@ { "begin": "\\b(transition(-(property|duration|delay|timing-function))?)\\b", "beginCaptures": { - "0": { - "name": "meta.property-name.less" - }, "1": { "name": "support.type.property-name.less" } @@ -3933,40 +4289,36 @@ }, "patterns": [ { - "captures": { + "begin": "(((\\+_?)?):)(?=[\\s\\t]*)", + "beginCaptures": { "1": { "name": "punctuation.separator.key-value.less" + } + }, + "contentName": "meta.property-value.less", + "end": "(?=\\s*(;)|(?=[})]))", + "patterns": [ + { + "include": "#time-type" }, - "4": { - "name": "meta.property-value.less" + { + "include": "#property-values" + }, + { + "include": "#cubic-bezier-function" + }, + { + "include": "#steps-function" + }, + { + "include": "#arbitrary-repetition" } - }, - "match": "(((\\+_?)?):)([\\s\\t]*)" - }, - { - "include": "#time-type" - }, - { - "include": "#property-values" - }, - { - "include": "#cubic-bezier-function" - }, - { - "include": "#steps-function" - }, - { - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, - "match": "\\s*(?:(,))" + ] } ] }, { - "begin": "\\bfilter\\b", + "begin": "\\b(?:backdrop-)?filter\\b", "beginCaptures": { "0": { "name": "support.type.property-name.less" @@ -4035,12 +4387,7 @@ "name": "support.constant.property-value.less" }, { - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, - "match": "\\s*(?:(,))" + "include": "#arbitrary-repetition" } ] }, @@ -4084,7 +4431,11 @@ ] }, { - "match": "(?x)\\b( accent-height | align-content | align-items | align-self | alignment-baseline | all | animation-timing-function | animation-play-state | animation-name | animation-iteration-count | animation-fill-mode | animation-duration | animation-direction | animation-delay | animation | appearance | ascent | azimuth | backface-visibility | background-size | background-repeat-y | background-repeat-x | background-repeat | background-position-y | background-position-x | background-position | background-origin | background-image | background-color | background-clip | background-blend-mode | background-attachment | background | baseline-shift | begin | bias | blend-mode | border-((top|right|bottom|left)-)?(width|style|color) | border-(top|bottom)-(right|left)-radius | border-image-(width|source|slice|repeat|outset) | border-(top|right|bottom|left|collapse|image|radius|spacing) | border | bottom | box-(align|decoration-break|direction|flex|ordinal-group|orient|pack|shadow|sizing) | break-(after|before|inside) | caption-side | clear | clip-path | clip-rule | clip | color(-(interpolation(-filters)?|profile|rendering))? | columns | column-(break-before|count|fill|gap|(rule(-(color|style|width))?)|span|width) | contain | content | counter-(increment|reset) | cursor | (c|d|f)(x|y) | direction | display | divisor | dominant-baseline | dur | elevation | empty-cells | enable-background | end | fallback | fill(-(opacity|rule))? | filter | flex(-(align|basis|direction|flow|grow|item-align|line-pack|negative|order|pack|positive|preferred-size|shrink|wrap))? | float | flood-(color|opacity) | font-display | font-family | font-feature-settings | font-kerning | font-language-override | font-size(-adjust)? | font-smoothing | font-stretch | font-style | font-synthesis | font-variant(-(alternates|caps|east-asian|ligatures|numeric|position))? | font-weight | font | fr | glyph-orientation-(horizontal|vertical) | grid-(area|gap) | grid-auto-(columns|flow|rows) | grid-(column|row)(-(end|gap|start))? | grid-template(-(areas|columns|rows))? | height | hyphens | image-(orientation|rendering|resolution) | isolation | justify-content | kerning | left | letter-spacing | lighting-color | line-(box-contain|break|clamp|height) | list-style(-(image|position|type))? | margin(-(bottom|left|right|top))? | marker(-(end|mid|start))? | mask(-(clip||composite|image|origin|position|repeat|size|type))? | (max|min)-(height|width) | mix-blend-mode | nbsp-mode | negative | object-(fit|position) | opacity | operator | order | orphans | outline(-(color|offset|style|width))? | overflow(-(scrolling|wrap|x|y))? | pad(ding(-(bottom|left|right|top))?)? | page(-break-(after|before|inside))? | paint-order | pause(-(after|before))? | perspective(-origin(-(x|y))?)? | pitch(-range)? | pointer-events | position | prefix | quotes | range | resize | right | rotate | scale | scroll-behavior | shape-(image-threshold|margin|outside|rendering) | size | speak(-as)? | src | stop-(color|opacity) | stroke(-(dash(array|offset)|line(cap|join)|miterlimit|opacity|width))? | suffix | symbols | system | tab-size | table-layout | tap-highlight-color | text-align(-last)? | text-decoration(-(color|line|style))? | text-emphasis(-(color|position|style))? | text-(anchor|fill-color|height|indent|justify|orientation|overflow|rendering|shadow|transform|underline-position) | top | touch-action | transform(-origin(-(x|y))?) | transform(-style)? | transition(-(delay|duration|property|timing-function))? | translate | unicode-(bidi|range) | user-(drag|select) | vertical-align | visibility | white-space | widows | width | will-change | word-(break|spacing|wrap) | writing-mode | z-index | zoom )\\b", + "match": "(?x)\\b( accent-height | align-content | align-items | align-self | alignment-baseline | all | animation-timing-function | animation-range-start | animation-range-end | animation-range | animation-play-state | animation-name | animation-iteration-count | animation-fill-mode | animation-duration | animation-direction | animation-delay | animation-composition | animation | appearance | ascent | azimuth | backface-visibility | background-size | background-repeat-y | background-repeat-x | background-repeat | background-position-y | background-position-x | background-position | background-origin | background-image | background-color | background-clip | background-blend-mode | background-attachment | background | baseline-shift | begin | bias | blend-mode | border-top-left-radius | border-top-right-radius | border-bottom-left-radius | border-bottom-right-radius | border-end-end-radius | border-end-start-radius | border-start-end-radius | border-start-start-radius | border-block-start-color | border-block-start-style | border-block-start-width | border-block-start | border-block-end-color | border-block-end-style | border-block-end-width | border-block-end | border-block-color | border-block-style | border-block-width | border-block | border-inline-start-color | border-inline-start-style | border-inline-start-width | border-inline-start | border-inline-end-color | border-inline-end-style | border-inline-end-width | border-inline-end | border-inline-color | border-inline-style | border-inline-width | border-inline | border-top-color | border-top-style | border-top-width | border-top | border-right-color | border-right-style | border-right-width | border-right | border-bottom-color | border-bottom-style | border-bottom-width | border-bottom | border-left-color | border-left-style | border-left-width | border-left | border-image-outset | border-image-repeat | border-image-slice | border-image-source | border-image-width | border-image | border-color | border-style | border-width | border-radius | border-collapse | border-spacing | border | bottom | box-(align|decoration-break|direction|flex|ordinal-group|orient|pack|shadow|sizing) | break-(after|before|inside) | caption-side | clear | clip-path | clip-rule | clip | color(-(interpolation(-filters)?|profile|rendering))? | columns | column-(break-before|count|fill|gap|(rule(-(color|style|width))?)|span|width) | contain(-intrinsic-((((block|inline)-)?size)|height|width))? | content | counter-(increment|reset) | cursor | (c|d|f)(x|y) | direction | display | divisor | dominant-baseline | dur | elevation | empty-cells | enable-background | end | fallback | fill(-(opacity|rule))? | filter | flex(-(align|basis|direction|flow|grow|item-align|line-pack|negative|order|pack|positive|preferred-size|shrink|wrap))? | float | flood-(color|opacity) | font-display | font-family | font-feature-settings | font-kerning | font-language-override | font-size(-adjust)? | font-smoothing | font-stretch | font-style | font-synthesis | font-variant(-(alternates|caps|east-asian|ligatures|numeric|position))? | font-weight | font | fr | ((column|row)-)?gap | glyph-orientation-(horizontal|vertical) | grid-(area|gap) | grid-auto-(columns|flow|rows) | grid-(column|row)(-(end|gap|start))? | grid-template(-(areas|columns|rows))? | height | hyphens | image-(orientation|rendering|resolution) | inset(-(block|inline))?(-(start|end))? | isolation | justify-content | justify-items | justify-self | kerning | left | letter-spacing | lighting-color | line-(box-contain|break|clamp|height) | list-style(-(image|position|type))? | (margin|padding)(-(bottom|left|right|top)|(-(block|inline)?(-(end|start))?))? | marker(-(end|mid|start))? | mask(-(clip||composite|image|origin|position|repeat|size|type))? | (max|min)-(height|width) | mix-blend-mode | nbsp-mode | negative | object-(fit|position) | opacity | operator | order | orphans | outline(-(color|offset|style|width))? | overflow(-((inline|block)|scrolling|wrap|x|y))? | overscroll-behavior(-block|-(inline|x|y))? | pad(ding(-(bottom|left|right|top))?)? | page(-break-(after|before|inside))? | paint-order | pause(-(after|before))? | perspective(-origin(-(x|y))?)? | pitch(-range)? | place-content | place-self | pointer-events | position | prefix | quotes | range | resize | right | rotate | scale | scroll-behavior | shape-(image-threshold|margin|outside|rendering) | size | speak(-as)? | src | stop-(color|opacity) | stroke(-(dash(array|offset)|line(cap|join)|miterlimit|opacity|width))? | suffix | symbols | system | tab-size | table-layout | tap-highlight-color | text-align(-last)? | text-decoration(-(color|line|style))? | text-emphasis(-(color|position|style))? | text-(anchor|fill-color|height|indent|justify|orientation|overflow|rendering|size-adjust|shadow|transform|underline-position|wrap) | top | touch-action | transform(-origin(-(x|y))?) | transform(-style)? | transition(-(delay|duration|property|timing-function))? | translate | unicode-(bidi|range) | user-(drag|select) | vertical-align | visibility | white-space(-collapse)? | widows | width | will-change | word-(break|spacing|wrap) | writing-mode | z-index | zoom )\\b", + "name": "support.type.property-name.less" + }, + { + "match": "(?x)\\b(((contain-intrinsic|max|min)-)?(block|inline)?-size)\\b", "name": "support.type.property-name.less" }, { @@ -4093,7 +4444,15 @@ ] }, { - "begin": "\\b(((\\+_?)?):)([\\s\\t]*)", + "begin": "\\b((?:(?:\\+_?)?):)([\\s\\t]*)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.less" + }, + "2": { + "name": "meta.property-value.less" + } + }, "captures": { "1": { "name": "punctuation.separator.key-value.less" @@ -4120,6 +4479,40 @@ } ] }, + "scroll-function": { + "begin": "\\b(scroll)(\\()", + "beginCaptures": { + "1": { + "name": "support.function.scroll.less" + }, + "2": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "match": "root|nearest|self", + "name": "support.constant.scroller.less" + }, + { + "match": "block|inline|x|y", + "name": "support.constant.axis.less" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + } + ] + }, "selector": { "patterns": [ { @@ -4140,13 +4533,7 @@ "include": "#less-variable-interpolation" }, { - "captures": { - "1": { - "name": "punctuation.separator.less" - } - }, - "match": "(\\!)\\s*important", - "name": "keyword.other.important.less" + "include": "#important" } ] } @@ -4288,12 +4675,7 @@ ] }, { - "captures": { - "1": { - "name": "punctuation.definition.arbitrary-repetition.less" - } - }, - "match": "\\s*(?:(,))" + "include": "#arbitrary-repetition" }, { "match": "\\*", @@ -4462,12 +4844,16 @@ ] }, "steps-function": { - "begin": "\\b(steps)(?=\\()", + "begin": "\\b(steps)(\\()", "beginCaptures": { - "0": { + "1": { "name": "support.function.timing.less" + }, + "2": { + "name": "punctuation.definition.group.begin.less" } }, + "contentName": "meta.group.less", "end": "\\)", "endCaptures": { "0": { @@ -4477,25 +4863,23 @@ "name": "meta.function-call.less", "patterns": [ { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.group.begin.less" - } - }, - "end": "(?=\\))", - "patterns": [ - { - "include": "#comma-delimiter" - }, - { - "include": "#integer-type" - }, - { - "match": "(end|middle|start)", - "name": "support.keyword.timing-direction.less" - } - ] + "match": "jump-start|jump-end|jump-none|jump-both|start|end", + "name": "support.constant.step-position.less" + }, + { + "include": "#comma-delimiter" + }, + { + "include": "#integer-type" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" } ] }, @@ -4968,42 +5352,49 @@ } ] }, + "value-separator": { + "captures": { + "1": { + "name": "punctuation.separator.less" + } + }, + "match": "\\s*(/)\\s*" + }, "var-function": { + "begin": "\\b(var)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.var.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", "patterns": [ { - "begin": "\\b(var)(?=\\()", + "begin": "\\(", "beginCaptures": { - "1": { - "name": "support.function.var.less" - } - }, - "end": "\\)", - "endCaptures": { "0": { - "name": "punctuation.definition.group.end.less" + "name": "punctuation.definition.group.begin.less" } }, - "name": "meta.function-call.less", + "end": "(?=\\))", "patterns": [ { - "begin": "\\(", - "beginCaptures": { - "0": { - "name": "punctuation.definition.group.begin.less" - } - }, - "end": "(?=\\))", - "patterns": [ - { - "include": "#comma-delimiter" - }, - { - "include": "#custom-property-name" - }, - { - "include": "#less-variables" - } - ] + "include": "#comma-delimiter" + }, + { + "include": "#custom-property-name" + }, + { + "include": "#less-variables" + }, + { + "include": "#property-values" } ] } @@ -5012,6 +5403,56 @@ "vendor-prefix": { "match": "-(?:webkit|moz(-osx)?|ms|o)-", "name": "support.type.vendor-prefix.less" + }, + "view-function": { + "begin": "\\b(view)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.view.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "match": "block|inline|x|y|auto", + "name": "support.constant.property-value.less" + }, + { + "include": "#length-type" + }, + { + "include": "#percentage-type" + }, + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" + }, + { + "include": "#arbitrary-repetition" + } + ] + } + ] } } } \ No newline at end of file diff --git a/extensions/lua/cgmanifest.json b/extensions/lua/cgmanifest.json index 50afc15d1cf..f35219d5a61 100644 --- a/extensions/lua/cgmanifest.json +++ b/extensions/lua/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "sumneko/lua.tmbundle", "repositoryUrl": "https://github.com/sumneko/lua.tmbundle", - "commitHash": "94ce82cc4d45f82641a5252d7a7fd9e28c875adc" + "commitHash": "1483add845ebfb3e1e631fe372603e5fed2cdd42" } }, "licenseDetail": [ diff --git a/extensions/lua/syntaxes/lua.tmLanguage.json b/extensions/lua/syntaxes/lua.tmLanguage.json index e578b8846cb..61875d06cf8 100644 --- a/extensions/lua/syntaxes/lua.tmLanguage.json +++ b/extensions/lua/syntaxes/lua.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/sumneko/lua.tmbundle/commit/94ce82cc4d45f82641a5252d7a7fd9e28c875adc", + "version": "https://github.com/sumneko/lua.tmbundle/commit/1483add845ebfb3e1e631fe372603e5fed2cdd42", "name": "Lua", "scopeName": "source.lua", "patterns": [ @@ -141,8 +141,8 @@ { "match": "<\\s*(const|close)\\s*>", "captures": { - "1": { - "name": "string.tag.lua" + "0": { + "name": "storage.type.attribute.lua" } } }, @@ -155,7 +155,7 @@ "name": "keyword.control.lua" }, { - "match": "\\b(local|global)\\b", + "match": "\\b(local)\\b", "name": "keyword.local.lua" }, { @@ -363,7 +363,7 @@ "name": "punctuation.definition.comment.begin.lua" } }, - "end": "\\]\\1\\]", + "end": "(--)?\\]\\1\\]", "endCaptures": { "0": { "name": "punctuation.definition.comment.end.lua" @@ -383,7 +383,7 @@ "name": "punctuation.definition.comment.begin.lua" } }, - "end": "\\]\\1\\]", + "end": "(--)?\\]\\1\\]", "endCaptures": { "0": { "name": "punctuation.definition.comment.end.lua" @@ -472,7 +472,7 @@ "emmydoc": { "patterns": [ { - "begin": "(?<=---[ \\t]*)@class", + "begin": "(?<=---)[ \\t]*@class", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -491,7 +491,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@enum", + "begin": "(?<=---)[ \\t]*@enum", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -511,7 +511,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@type", + "begin": "(?<=---)[ \\t]*@type", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -525,7 +525,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@alias", + "begin": "(?<=---)[ \\t]*@alias", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -550,7 +550,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)(@operator)\\s*(\\b[a-z]+)?", + "begin": "(?<=---)[ \\t]*(@operator)\\s*(\\b[a-z]+)?", "beginCaptures": { "1": { "name": "storage.type.annotation.lua" @@ -567,7 +567,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@cast", + "begin": "(?<=---)[ \\t]*@cast", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -596,7 +596,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@param", + "begin": "(?<=---)[ \\t]*@param", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -624,7 +624,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@return", + "begin": "(?<=---)[ \\t]*@return", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -642,7 +642,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@field", + "begin": "(?<=---)[ \\t]*@field", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -677,7 +677,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@generic", + "begin": "(?<=---)[ \\t]*@generic", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -711,7 +711,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@vararg", + "begin": "(?<=---)[ \\t]*@vararg", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -725,7 +725,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@overload", + "begin": "(?<=---)[ \\t]*@overload", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -739,7 +739,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@deprecated", + "begin": "(?<=---)[ \\t]*@deprecated", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -748,7 +748,7 @@ "end": "(?=[\\n@#])" }, { - "begin": "(?<=---[ \\t]*)@meta", + "begin": "(?<=---)[ \\t]*@meta", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -757,7 +757,7 @@ "end": "(?=[\\n@#])" }, { - "begin": "(?<=---[ \\t]*)@private", + "begin": "(?<=---)[ \\t]*@private", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -766,7 +766,7 @@ "end": "(?=[\\n@#])" }, { - "begin": "(?<=---[ \\t]*)@protected", + "begin": "(?<=---)[ \\t]*@protected", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -775,7 +775,7 @@ "end": "(?=[\\n@#])" }, { - "begin": "(?<=---[ \\t]*)@package", + "begin": "(?<=---)[ \\t]*@package", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -784,7 +784,7 @@ "end": "(?=[\\n@#])" }, { - "begin": "(?<=---[ \\t]*)@version", + "begin": "(?<=---)[ \\t]*@version", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -803,7 +803,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@see", + "begin": "(?<=---)[ \\t]*@see", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -822,7 +822,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@diagnostic", + "begin": "(?<=---)[ \\t]*@diagnostic", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -855,7 +855,7 @@ ] }, { - "begin": "(?<=---[ \\t]*)@module", + "begin": "(?<=---)[ \\t]*@module", "beginCaptures": { "0": { "name": "storage.type.annotation.lua" @@ -869,7 +869,7 @@ ] }, { - "match": "(?<=---[ \\t]*)@(async|nodiscard)", + "match": "(?<=---)[ \\t]*@(async|nodiscard)", "name": "storage.type.annotation.lua" }, { diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 60c6b192bed..380b0c74ac6 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "f75d5f55730e72ee7ff386841949048b2395e440" + "commitHash": "7418dd20d76c72e82fadee2909e03239e9973b35" } }, "license": "MIT", diff --git a/extensions/markdown-basics/language-configuration.json b/extensions/markdown-basics/language-configuration.json index f1e7859ccca..6e1766db02c 100644 --- a/extensions/markdown-basics/language-configuration.json +++ b/extensions/markdown-basics/language-configuration.json @@ -79,6 +79,10 @@ [ "<", ">" + ], + [ + "~", + "~" ] ], "folding": { diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index c84c468b80c..9761ca716ab 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/f75d5f55730e72ee7ff386841949048b2395e440", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/7418dd20d76c72e82fadee2909e03239e9973b35", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -2480,14 +2480,34 @@ "name": "meta.separator.markdown" }, "frontMatter": { - "begin": "\\A-{3}\\s*$", - "contentName": "meta.embedded.block.frontmatter", + "begin": "\\A(?=(-{3,}))", + "end": "^ {,3}\\1-*[ \\t]*$|^[ \\t]*\\.{3}$", + "applyEndPatternLast": 1, + "endCaptures": { + "0": { + "name": "punctuation.definition.end.frontmatter" + } + }, "patterns": [ { - "include": "source.yaml" + "begin": "\\A(-{3,})(.*)$", + "while": "^(?! {,3}\\1-*[ \\t]*$|[ \\t]*\\.{3}$)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.begin.frontmatter" + }, + "2": { + "name": "comment.frontmatter" + } + }, + "contentName": "meta.embedded.block.frontmatter", + "patterns": [ + { + "include": "source.yaml" + } + ] } - ], - "end": "(^|\\G)-{3}|\\.{3}\\s*$" + ] }, "table": { "name": "markup.table.markdown", diff --git a/extensions/markdown-language-features/.vscodeignore b/extensions/markdown-language-features/.vscodeignore index 588916d37c3..fdbb001a79a 100644 --- a/extensions/markdown-language-features/.vscodeignore +++ b/extensions/markdown-language-features/.vscodeignore @@ -15,11 +15,4 @@ webpack.config.js esbuild-notebook.js esbuild-preview.js .gitignore -server/src/** -server/extension.webpack.config.js -server/extension-browser.webpack.config.js -server/tsconfig.json -server/.vscode/** -server/node_modules/** -server/yarn.lock -server/.npmignore +**/*.d.ts diff --git a/extensions/markdown-language-features/extension-browser.webpack.config.js b/extensions/markdown-language-features/extension-browser.webpack.config.js index bad27aab02e..47f50aa5e8f 100644 --- a/extensions/markdown-language-features/extension-browser.webpack.config.js +++ b/extensions/markdown-language-features/extension-browser.webpack.config.js @@ -7,13 +7,25 @@ 'use strict'; -const withBrowserDefaults = require('../shared.webpack.config').browser; +const CopyPlugin = require('copy-webpack-plugin'); +const { browserPlugins, browser } = require('../shared.webpack.config'); -module.exports = withBrowserDefaults({ +module.exports = browser({ context: __dirname, entry: { extension: './src/extension.browser.ts' - } + }, + plugins: [ + ...browserPlugins(__dirname), // add plugins, don't replace inherited + new CopyPlugin({ + patterns: [ + { + from: './node_modules/vscode-markdown-languageserver/dist/browser/workerMain.js', + to: 'serverWorkerMain.js', + } + ], + }), + ], }, { configFile: 'tsconfig.browser.json' }); diff --git a/extensions/markdown-language-features/extension.webpack.config.js b/extensions/markdown-language-features/extension.webpack.config.js index de88398eca0..588d0632fd2 100644 --- a/extensions/markdown-language-features/extension.webpack.config.js +++ b/extensions/markdown-language-features/extension.webpack.config.js @@ -7,6 +7,7 @@ 'use strict'; +const CopyPlugin = require('copy-webpack-plugin'); const withDefaults = require('../shared.webpack.config'); module.exports = withDefaults({ @@ -16,5 +17,16 @@ module.exports = withDefaults({ }, entry: { extension: './src/extension.ts', - } + }, + plugins: [ + ...withDefaults.nodePlugins(__dirname), // add plugins, don't replace inherited + new CopyPlugin({ + patterns: [ + { + from: './node_modules/vscode-markdown-languageserver/dist/node/workerMain.js', + to: 'serverWorkerMain.js', + } + ], + }), + ], }); diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 168f6a8a862..800be985a43 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -205,7 +205,7 @@ table > tbody > tr + tr > td { blockquote { margin: 0; - padding: 2px 16px 0 10px; + padding: 0px 16px 0 10px; border-left-width: 5px; border-left-style: solid; border-radius: 2px; diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index a80b0294786..894c1035c39 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -16,8 +16,7 @@ "Programming Languages" ], "enabledApiProposals": [ - "documentPaste", - "dropMetadata" + "documentPaste" ], "activationEvents": [ "onLanguage:markdown", @@ -495,7 +494,7 @@ ] }, "markdown.editor.filePaste.enabled": { - "type": "boolean", + "type": "string", "scope": "resource", "markdownDescription": "%configuration.markdown.editor.filePaste.enabled%", "default": "smart", @@ -708,6 +707,12 @@ "%configuration.markdown.preferredMdPathExtensionStyle.includeExtension%", "%configuration.markdown.preferredMdPathExtensionStyle.removeExtension%" ] + }, + "markdown.editor.updateLinksOnPaste.enabled": { + "type": "boolean", + "markdownDescription": "%configuration.markdown.editor.updateLinksOnPaste.enabled%", + "scope": "resource", + "default": true } } }, @@ -748,8 +753,8 @@ ] }, "scripts": { - "compile": "gulp compile-extension:markdown-language-features-languageService && gulp compile-extension:markdown-language-features-server && gulp compile-extension:markdown-language-features && npm run build-preview && npm run build-notebook", - "watch": "npm run build-preview && gulp watch-extension:markdown-language-features watch-extension:markdown-language-features-languageService watch-extension:markdown-language-features-server", + "compile": "gulp compile-extension:markdown-language-features-languageService && gulp compile-extension:markdown-language-features && npm run build-preview && npm run build-notebook", + "watch": "npm run build-preview && gulp watch-extension:markdown-language-features watch-extension:markdown-language-features-languageService", "vscode:prepublish": "npm run build-ext && npm run build-preview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json", "build-notebook": "node ./esbuild-notebook", @@ -762,11 +767,12 @@ "dompurify": "^3.0.5", "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", - "markdown-it-front-matter": "^0.2.1", + "markdown-it-front-matter": "^0.2.4", "morphdom": "^2.6.1", "picomatch": "^2.3.1", "vscode-languageclient": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.11", + "vscode-markdown-languageserver": "^0.5.0-alpha.8", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index a4b25260ba8..da567b46665 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -91,5 +91,6 @@ "configuration.markdown.preferredMdPathExtensionStyle.removeExtension": "Prefer removing the file extension. For example, path completions to a file named `file.md` will insert `file` without the `.md`.", "configuration.markdown.editor.filePaste.videoSnippet": "Snippet used when adding videos to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the video file.\n- `${title}` — The title used for the video. A snippet placeholder will automatically be created for this variable.", "configuration.markdown.editor.filePaste.audioSnippet": "Snippet used when adding audio to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the audio file.\n- `${title}` — The title used for the audio. A snippet placeholder will automatically be created for this variable.", + "configuration.markdown.editor.updateLinksOnPaste.enabled": "Enable/disable a paste option that updates links and reference in text that is copied and pasted between Markdown editors.\n\nTo use this feature, after pasting text that contains updatable links, just click on the Paste Widget and select `Paste and update pasted links`.", "workspaceTrust": "Required for loading styles configured in the workspace." } diff --git a/extensions/markdown-language-features/schemas/package.schema.json b/extensions/markdown-language-features/schemas/package.schema.json index 5591d0b0032..8dea48f757f 100644 --- a/extensions/markdown-language-features/schemas/package.schema.json +++ b/extensions/markdown-language-features/schemas/package.schema.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Markdown contributions to package.json", "type": "object", "properties": { "contributes": { diff --git a/extensions/markdown-language-features/server/.npmignore b/extensions/markdown-language-features/server/.npmignore deleted file mode 100644 index bfd4215998c..00000000000 --- a/extensions/markdown-language-features/server/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -.vscode/ -.github/ -out/test/ -src/ -.eslintrc.js -.gitignore -tsconfig*.json -*.tsbuildinfo -*.map -example.cjs -CODE_OF_CONDUCT.md -SECURITY.md \ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/launch.json b/extensions/markdown-language-features/server/.vscode/launch.json deleted file mode 100644 index fd9033bffaa..00000000000 --- a/extensions/markdown-language-features/server/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "0.1.0", - // List of configurations. Add new configurations or edit existing ones. - "configurations": [ - { - "name": "Attach", - "type": "node", - "request": "attach", - "port": 7997, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ] - } - ] -} \ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/settings.json b/extensions/markdown-language-features/server/.vscode/settings.json deleted file mode 100644 index 7a73a41bfdf..00000000000 --- a/extensions/markdown-language-features/server/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/tasks.json b/extensions/markdown-language-features/server/.vscode/tasks.json deleted file mode 100644 index ecc951a7baf..00000000000 --- a/extensions/markdown-language-features/server/.vscode/tasks.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": "2.0.0", - "command": "npm", - "args": [ - "run", - "watch" - ], - "isBackground": true, - "problemMatcher": "$tsc-watch", - "tasks": [ - { - "label": "npm", - "type": "shell", - "command": "npm", - "args": [ - "run", - "watch" - ], - "isBackground": true, - "problemMatcher": "$tsc-watch", - "group": { - "_id": "build", - "isDefault": false - } - } - ] -} \ No newline at end of file diff --git a/extensions/markdown-language-features/server/CHANGELOG.md b/extensions/markdown-language-features/server/CHANGELOG.md deleted file mode 100644 index a5cc9d15cf1..00000000000 --- a/extensions/markdown-language-features/server/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -# 0.4.0-alpha.3 — June 2, 2023 -- Pick up [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) 0.4.0-alpha.3. See [CHANGELOG](https://github.com/microsoft/vscode-markdown-languageservice/blob/main/CHANGELOG.md#040-alpha3--may-30-2023) for details. - -## 0.3.0 - March 28, 2023 -- Pick up [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) 0.3.0. See [CHANGELOG](https://github.com/microsoft/vscode-markdown-languageservice/blob/main/CHANGELOG.md#030--march-16-2023) for details. diff --git a/extensions/markdown-language-features/server/README.md b/extensions/markdown-language-features/server/README.md deleted file mode 100644 index 4114d2698bf..00000000000 --- a/extensions/markdown-language-features/server/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# Markdown Language Server - -> **❗ Import** This is still in development. While the language server is being used by VS Code, it has not yet been tested with other clients. - -The Markdown language server powers VS Code's built-in markdown support, providing tools for writing and browsing Markdown files. It runs as a separate executable and implements the [language server protocol](https://microsoft.github.io/language-server-protocol/overview). - -This server uses the [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) to implement almost all of the language features. You can use that library if you need a library for working with Markdown instead of a full language server. - -## Server capabilities - -- [Completions](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) for Markdown links. - -- [Folding](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange) of Markdown regions, block elements, and header sections. - -- [Smart selection](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange) for inline elements, block elements, and header sections. - -- [Document Symbols](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for quick navigation to headers in a document. - -- [Workspace Symbols](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) for quick navigation to headers in the workspace - -- [Document links](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentLink) for making Markdown links in a document clickable. - -- [Find all references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) to headers and links across all Markdown files in the workspace. - -- [Go to definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) from links to headers or link definitions. - -- [Rename](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename) of headers and links across all Markdown files in the workspace. - -- Find all references to a file. Uses a custom `markdown/getReferencesToFileInWorkspace` message. - -- [Code Actions](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction) - - - Organize link definitions source action. - - Extract link to definition refactoring. - -- Updating links when a file is moved / renamed. Uses a custom `markdown/getEditForFileRenames` message. - -- [Pull diagnostics (validation)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) for links. - -## Client requirements - -### Initialization options - -The client can send the following initialization options to the server: - -- `markdownFileExtensions` Array file extensions that should be considered as Markdown. These should not include the leading `.`. For example: `['md', 'mdown', 'markdown']`. - -### Settings - -Clients may send a `workspace/didChangeConfiguration` notification to notify the server of settings changes. -The server supports the following settings: - -- `markdown` - - `suggest` - - `paths` - - `enabled` — Enable/disable path suggestions. - - - `occurrencesHighlight` - - `enabled` — Enable/disable highlighting of link occurrences. - - - `validate` - - `enabled` — Enable/disable all validation. - - `referenceLinks` - - `enabled` — Enable/disable validation of reference links: `[text][ref]` - - `fragmentLinks` - - `enabled` — Enable/disable validation of links to fragments in the current files: `[text](#head)` - - `fileLinks` - - `enabled` — Enable/disable validation of links to file in the workspace. - - `markdownFragmentLinks` — Enable/disable validation of links to headers in other Markdown files. Use `inherit` to inherit the `fragmentLinks` setting. - - `ignoredLinks` — Array of glob patterns for files that should not be validated. - - `unusedLinkDefinitions` - - `enabled` — Enable/disable validation of unused link definitions. - - `duplicateLinkDefinitions` - - `enabled` — Enable/disable validation of duplicated link definitions. - -### Custom requests - -To support all of the features of the language server, the client needs to implement a few custom request types. The definitions of these request types can be found in [`protocol.ts`](./src/protocol.ts) - -#### `markdown/parse` - -Get the tokens for a Markdown file. Clients are expected to use [Markdown-it](https://github.com/markdown-it/markdown-it) for this. - -We require that clients bring their own version of Markdown-it so that they can customize/extend Markdown-it. - -#### `markdown/fs/readFile` - -Read the contents of a file in the workspace. - -#### `markdown/fs/readDirectory` - -Read the contents of a directory in the workspace. - -#### `markdown/fs/stat` - -Check if a given file/directory exists in the workspace. - -#### `markdown/fs/watcher/create` - -Create a file watcher. This is needed for diagnostics support. - -#### `markdown/fs/watcher/delete` - -Delete a previously created file watcher. - -#### `markdown/findMarkdownFilesInWorkspace` - -Get a list of all markdown files in the workspace. - -## Contribute - -The source code of the Markdown language server can be found in the [VSCode repository](https://github.com/microsoft/vscode) at [extensions/markdown-language-features/server](https://github.com/microsoft/vscode/tree/master/extensions/markdown-language-features/server). - -File issues and pull requests in the [VSCode GitHub Issues](https://github.com/microsoft/vscode/issues). See the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) on how to build and run from source. - -Most of the functionality of the server is located in libraries: - -- [vscode-markdown-languageservice](https://github.com/microsoft/vscode-markdown-languageservice) contains the implementation of all features as a reusable library. -- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) contains the implementation of language server for NodeJS. - -Help on any of these projects is very welcome. - -## Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## License - -Copyright (c) Microsoft Corporation. All rights reserved. - -Licensed under the [MIT](https://github.com/microsoft/vscode/blob/master/LICENSE.txt) License. diff --git a/extensions/markdown-language-features/server/build/pipeline.yml b/extensions/markdown-language-features/server/build/pipeline.yml deleted file mode 100644 index 0c9e3bdbd11..00000000000 --- a/extensions/markdown-language-features/server/build/pipeline.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: $(Date:yyyyMMdd)$(Rev:.r) - -trigger: none -pr: none - -resources: - repositories: - - repository: templates - type: github - name: microsoft/vscode-engineering - ref: main - endpoint: Monaco - -parameters: - - name: publishPackage - displayName: Publish vscode-markdown-languageserver - type: boolean - default: false - -extends: - template: azure-pipelines/npm-package/pipeline.yml@templates - parameters: - npmPackages: - - name: vscode-markdown-languageserver - workingDirectory: extensions/markdown-language-features/server - - buildSteps: - - script: yarn install - displayName: Install dependencies - - - script: gulp compile-extension:markdown-language-features-server - displayName: Compile - - publishPackage: ${{ parameters.publishPackage }} - packagePlatform: 'Windows' diff --git a/extensions/markdown-language-features/server/extension-browser.webpack.config.js b/extensions/markdown-language-features/server/extension-browser.webpack.config.js deleted file mode 100644 index 2a9de70bc01..00000000000 --- a/extensions/markdown-language-features/server/extension-browser.webpack.config.js +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check - -'use strict'; - -const withBrowserDefaults = require('../../shared.webpack.config').browser; -const path = require('path'); - -module.exports = withBrowserDefaults({ - context: __dirname, - entry: { - extension: './src/browser/workerMain.ts', - }, - output: { - filename: 'workerMain.js', - path: path.join(__dirname, 'dist', 'browser'), - libraryTarget: 'var', - library: 'serverExportVar' - } -}); diff --git a/extensions/markdown-language-features/server/extension.webpack.config.js b/extensions/markdown-language-features/server/extension.webpack.config.js deleted file mode 100644 index aafc9c1fd96..00000000000 --- a/extensions/markdown-language-features/server/extension.webpack.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check - -'use strict'; - -const withDefaults = require('../../shared.webpack.config'); -const path = require('path'); - -module.exports = withDefaults({ - context: path.join(__dirname), - entry: { - extension: './src/node/workerMain.ts', - }, - output: { - filename: 'workerMain.js', - path: path.join(__dirname, 'dist', 'node'), - } -}); diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json deleted file mode 100644 index 175620ee8de..00000000000 --- a/extensions/markdown-language-features/server/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "vscode-markdown-languageserver", - "description": "Markdown language server", - "version": "0.4.0", - "author": "Microsoft Corporation", - "license": "MIT", - "engines": { - "node": "*" - }, - "main": "./out/node/main", - "browser": "./dist/browser/main", - "files": [ - "dist/**/*.js", - "out/**/*.js" - ], - "dependencies": { - "@vscode/l10n": "^0.0.11", - "vscode-languageserver": "^8.1.0", - "vscode-languageserver-textdocument": "^1.0.8", - "vscode-languageserver-types": "^3.17.3", - "vscode-markdown-languageservice": "^0.5.0-alpha.1", - "vscode-uri": "^3.0.7" - }, - "devDependencies": { - "@types/node": "18.x" - }, - "scripts": { - "compile": "gulp compile-extension:markdown-language-features-server", - "prepublishOnly": "npm run compile", - "watch": "gulp watch-extension:markdown-language-features-server", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" - } -} diff --git a/extensions/markdown-language-features/server/src/browser/main.ts b/extensions/markdown-language-features/server/src/browser/main.ts deleted file mode 100644 index 126121080a8..00000000000 --- a/extensions/markdown-language-features/server/src/browser/main.ts +++ /dev/null @@ -1,14 +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 { BrowserMessageReader, BrowserMessageWriter, createConnection } from 'vscode-languageserver/browser'; -import { startVsCodeServer } from '../server'; - -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); - -const connection = createConnection(messageReader, messageWriter); - -startVsCodeServer(connection); diff --git a/extensions/markdown-language-features/server/src/browser/workerMain.ts b/extensions/markdown-language-features/server/src/browser/workerMain.ts deleted file mode 100644 index e653751af40..00000000000 --- a/extensions/markdown-language-features/server/src/browser/workerMain.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as l10n from '@vscode/l10n'; - -let initialized = false; -const pendingMessages: any[] = []; - -const messageHandler = async (e: any) => { - if (!initialized) { - const l10nLog: string[] = []; - initialized = true; - const i10lLocation = e.data.i10lLocation; - if (i10lLocation) { - try { - await l10n.config({ uri: i10lLocation }); - l10nLog.push(`l10n: Configured to ${i10lLocation.toString()}.`); - } catch (e) { - l10nLog.push(`l10n: Problems loading ${i10lLocation.toString()} : ${e}.`); - } - } else { - l10nLog.push(`l10n: No bundle configured.`); - } - - await import('./main'); - - if (self.onmessage !== messageHandler) { - pendingMessages.forEach(msg => self.onmessage?.(msg)); - pendingMessages.length = 0; - } - - l10nLog.forEach(console.log); - } else { - pendingMessages.push(e); - } -}; -self.onmessage = messageHandler; diff --git a/extensions/markdown-language-features/server/src/config.ts b/extensions/markdown-language-features/server/src/config.ts deleted file mode 100644 index 5992258b0b6..00000000000 --- a/extensions/markdown-language-features/server/src/config.ts +++ /dev/null @@ -1,32 +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 { LsConfiguration } from 'vscode-markdown-languageservice'; - -export { LsConfiguration }; - -const defaultConfig: LsConfiguration = { - markdownFileExtensions: ['md'], - knownLinkedToFileExtensions: [ - 'jpg', - 'jpeg', - 'png', - 'gif', - 'webp', - 'bmp', - 'tiff', - ], - excludePaths: [ - '**/.*', - '**/node_modules/**', - ] -}; - -export function getLsConfiguration(overrides: Partial): LsConfiguration { - return { - ...defaultConfig, - ...overrides, - }; -} diff --git a/extensions/markdown-language-features/server/src/configuration.ts b/extensions/markdown-language-features/server/src/configuration.ts deleted file mode 100644 index 949573cfaf5..00000000000 --- a/extensions/markdown-language-features/server/src/configuration.ts +++ /dev/null @@ -1,74 +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 { Connection, Emitter } from 'vscode-languageserver'; -import { Disposable } from './util/dispose'; - -export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; - -export interface Settings { - readonly markdown: { - readonly server: { - readonly log: 'off' | 'debug' | 'trace'; - }; - - readonly preferredMdPathExtensionStyle: 'auto' | 'includeExtension' | 'removeExtension'; - - readonly occurrencesHighlight: { - readonly enabled: boolean; - }; - - readonly suggest: { - readonly paths: { - readonly enabled: boolean; - readonly includeWorkspaceHeaderCompletions: 'never' | 'onSingleOrDoubleHash' | 'onDoubleHash'; - }; - }; - - readonly validate: { - readonly enabled: true; - readonly referenceLinks: { - readonly enabled: ValidateEnabled; - }; - readonly fragmentLinks: { - readonly enabled: ValidateEnabled; - }; - readonly fileLinks: { - readonly enabled: ValidateEnabled; - readonly markdownFragmentLinks: ValidateEnabled | 'inherit'; - }; - readonly ignoredLinks: readonly string[]; - readonly unusedLinkDefinitions: { - readonly enabled: ValidateEnabled; - }; - readonly duplicateLinkDefinitions: { - readonly enabled: ValidateEnabled; - }; - }; - }; -} - - -export class ConfigurationManager extends Disposable { - - private readonly _onDidChangeConfiguration = this._register(new Emitter()); - public readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - - private _settings?: Settings; - - constructor(connection: Connection) { - super(); - - // The settings have changed. Is send on server activation as well. - this._register(connection.onDidChangeConfiguration((change) => { - this._settings = change.settings; - this._onDidChangeConfiguration.fire(this._settings!); - })); - } - - public getSettings(): Settings | undefined { - return this._settings; - } -} diff --git a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts deleted file mode 100644 index d21a6fdbfb6..00000000000 --- a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts +++ /dev/null @@ -1,114 +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 { Connection, FullDocumentDiagnosticReport, TextDocuments, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver'; -import * as md from 'vscode-markdown-languageservice'; -import { Disposable } from 'vscode-notebook-renderer/events'; -import { URI } from 'vscode-uri'; -import { ConfigurationManager, ValidateEnabled } from '../configuration'; -import { disposeAll } from '../util/dispose'; - -const defaultDiagnosticOptions: md.DiagnosticOptions = { - validateFileLinks: md.DiagnosticLevel.ignore, - validateReferences: md.DiagnosticLevel.ignore, - validateFragmentLinks: md.DiagnosticLevel.ignore, - validateMarkdownFileLinkFragments: md.DiagnosticLevel.ignore, - validateUnusedLinkDefinitions: md.DiagnosticLevel.ignore, - validateDuplicateLinkDefinitions: md.DiagnosticLevel.ignore, - ignoreLinks: [], -}; - -function convertDiagnosticLevel(enabled: ValidateEnabled): md.DiagnosticLevel | undefined { - switch (enabled) { - case 'error': return md.DiagnosticLevel.error; - case 'warning': return md.DiagnosticLevel.warning; - case 'ignore': return md.DiagnosticLevel.ignore; - case 'hint': return md.DiagnosticLevel.hint; - default: return md.DiagnosticLevel.ignore; - } -} - -function getDiagnosticsOptions(config: ConfigurationManager): md.DiagnosticOptions { - const settings = config.getSettings(); - if (!settings) { - return defaultDiagnosticOptions; - } - - const validateFragmentLinks = convertDiagnosticLevel(settings.markdown.validate.fragmentLinks.enabled); - return { - validateFileLinks: convertDiagnosticLevel(settings.markdown.validate.fileLinks.enabled), - validateReferences: convertDiagnosticLevel(settings.markdown.validate.referenceLinks.enabled), - validateFragmentLinks: convertDiagnosticLevel(settings.markdown.validate.fragmentLinks.enabled), - validateMarkdownFileLinkFragments: settings.markdown.validate.fileLinks.markdownFragmentLinks === 'inherit' ? validateFragmentLinks : convertDiagnosticLevel(settings.markdown.validate.fileLinks.markdownFragmentLinks), - validateUnusedLinkDefinitions: convertDiagnosticLevel(settings.markdown.validate.unusedLinkDefinitions.enabled), - validateDuplicateLinkDefinitions: convertDiagnosticLevel(settings.markdown.validate.duplicateLinkDefinitions.enabled), - ignoreLinks: settings.markdown.validate.ignoredLinks, - }; -} - -export function registerValidateSupport( - connection: Connection, - workspace: md.IWorkspace, - documents: TextDocuments, - ls: md.IMdLanguageService, - config: ConfigurationManager, - logger: md.ILogger, -): Disposable { - let diagnosticOptions: md.DiagnosticOptions = defaultDiagnosticOptions; - function updateDiagnosticsSetting(): void { - diagnosticOptions = getDiagnosticsOptions(config); - } - - const subs: Disposable[] = []; - const manager = ls.createPullDiagnosticsManager(); - subs.push(manager); - - subs.push(manager.onLinkedToFileChanged(() => { - // TODO: We only need to refresh certain files - connection.languages.diagnostics.refresh(); - })); - - const emptyDiagnosticsResponse = Object.freeze({ kind: 'full', items: [] }); - - connection.languages.diagnostics.on(async (params, token): Promise => { - logger.log(md.LogLevel.Debug, 'connection.languages.diagnostics.on', { document: params.textDocument.uri }); - - if (!config.getSettings()?.markdown.validate.enabled) { - return emptyDiagnosticsResponse; - } - - const uri = URI.parse(params.textDocument.uri); - if (!workspace.hasMarkdownDocument(uri)) { - return emptyDiagnosticsResponse; - } - - const document = await workspace.openMarkdownDocument(uri); - if (!document) { - return emptyDiagnosticsResponse; - } - - const diagnostics = await manager.computeDiagnostics(document, diagnosticOptions, token); - return { - kind: 'full', - items: diagnostics, - }; - }); - - updateDiagnosticsSetting(); - subs.push(config.onDidChangeConfiguration(() => { - updateDiagnosticsSetting(); - connection.languages.diagnostics.refresh(); - })); - - subs.push(documents.onDidClose(e => { - manager.disposeDocumentResources(URI.parse(e.document.uri)); - })); - - return { - dispose: () => { - disposeAll(subs); - } - }; -} diff --git a/extensions/markdown-language-features/server/src/logging.ts b/extensions/markdown-language-features/server/src/logging.ts deleted file mode 100644 index 0df6b8e0cc5..00000000000 --- a/extensions/markdown-language-features/server/src/logging.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as md from 'vscode-markdown-languageservice'; -import { ConfigurationManager } from './configuration'; -import { Disposable } from './util/dispose'; - -export class LogFunctionLogger extends Disposable implements md.ILogger { - - private static now(): string { - const now = new Date(); - return String(now.getUTCHours()).padStart(2, '0') - + ':' + String(now.getMinutes()).padStart(2, '0') - + ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0'); - } - - private static data2String(data: any): string { - if (data instanceof Error) { - if (typeof data.stack === 'string') { - return data.stack; - } - return data.message; - } - if (typeof data === 'string') { - return data; - } - return JSON.stringify(data, undefined, 2); - } - - private _logLevel: md.LogLevel; - - constructor( - private readonly _logFn: typeof console.log, - private readonly _config: ConfigurationManager, - ) { - super(); - - this._register(this._config.onDidChangeConfiguration(() => { - this._logLevel = LogFunctionLogger.readLogLevel(this._config); - })); - - this._logLevel = LogFunctionLogger.readLogLevel(this._config); - } - - private static readLogLevel(config: ConfigurationManager): md.LogLevel { - switch (config.getSettings()?.markdown.server.log) { - case 'trace': return md.LogLevel.Trace; - case 'debug': return md.LogLevel.Debug; - case 'off': - default: - return md.LogLevel.Off; - } - } - - get level(): md.LogLevel { return this._logLevel; } - - public log(level: md.LogLevel, message: string, data?: any): void { - if (this.level < level) { - return; - } - - this.appendLine(`[${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); - if (data) { - this.appendLine(LogFunctionLogger.data2String(data)); - } - } - - private toLevelLabel(level: md.LogLevel): string { - switch (level) { - case md.LogLevel.Off: return 'Off'; - case md.LogLevel.Debug: return 'Debug'; - case md.LogLevel.Trace: return 'Trace'; - } - } - - private appendLine(value: string): void { - this._logFn(value); - } -} diff --git a/extensions/markdown-language-features/server/src/node/main.ts b/extensions/markdown-language-features/server/src/node/main.ts deleted file mode 100644 index 7945d44acf6..00000000000 --- a/extensions/markdown-language-features/server/src/node/main.ts +++ /dev/null @@ -1,19 +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 { Connection, createConnection } from 'vscode-languageserver/node'; -import { startVsCodeServer } from '../server'; - -// Create a connection for the server. -const connection: Connection = createConnection(); - -console.log = connection.console.log.bind(connection.console); -console.error = connection.console.error.bind(connection.console); - -process.on('unhandledRejection', (e: any) => { - connection.console.error(`Unhandled exception ${e}`); -}); - -startVsCodeServer(connection); diff --git a/extensions/markdown-language-features/server/src/node/workerMain.ts b/extensions/markdown-language-features/server/src/node/workerMain.ts deleted file mode 100644 index f3369768012..00000000000 --- a/extensions/markdown-language-features/server/src/node/workerMain.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as l10n from '@vscode/l10n'; - -async function setupMain() { - const l10nLog: string[] = []; - - const i10lLocation = process.env['VSCODE_L10N_BUNDLE_LOCATION']; - if (i10lLocation) { - try { - await l10n.config({ uri: i10lLocation }); - l10nLog.push(`l10n: Configured to ${i10lLocation.toString()}`); - } catch (e) { - l10nLog.push(`l10n: Problems loading ${i10lLocation.toString()} : ${e}`); - } - } - await import('./main'); - - l10nLog.forEach(console.log); -} -setupMain(); diff --git a/extensions/markdown-language-features/server/src/protocol.ts b/extensions/markdown-language-features/server/src/protocol.ts deleted file mode 100644 index e1dc9aee785..00000000000 --- a/extensions/markdown-language-features/server/src/protocol.ts +++ /dev/null @@ -1,30 +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 { FileRename, RequestType } from 'vscode-languageserver'; -import type * as lsp from 'vscode-languageserver-types'; -import type * as md from 'vscode-markdown-languageservice'; - -//#region From server -export const parse = new RequestType<{ uri: string }, md.Token[], any>('markdown/parse'); - -export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile'); -export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory'); -export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat'); - -export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions; watchParentDirs: boolean }, void, any>('markdown/fs/watcher/create'); -export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete'); - -export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace'); -//#endregion - -//#region To server -export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace'); -export const getEditForFileRenames = new RequestType('markdown/getEditForFileRenames'); - -export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange'); - -export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, md.ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget'); -//#endregion diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts deleted file mode 100644 index 50c34378471..00000000000 --- a/extensions/markdown-language-features/server/src/server.ts +++ /dev/null @@ -1,363 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as l10n from '@vscode/l10n'; -import { CancellationToken, CompletionRegistrationOptions, CompletionRequest, Connection, Disposable, DocumentHighlightRegistrationOptions, DocumentHighlightRequest, InitializeParams, InitializeResult, NotebookDocuments, ResponseError, TextDocuments } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import * as lsp from 'vscode-languageserver-types'; -import * as md from 'vscode-markdown-languageservice'; -import { URI } from 'vscode-uri'; -import { LsConfiguration, getLsConfiguration } from './config'; -import { ConfigurationManager, Settings } from './configuration'; -import { registerValidateSupport } from './languageFeatures/diagnostics'; -import { LogFunctionLogger } from './logging'; -import * as protocol from './protocol'; -import { IDisposable } from './util/dispose'; -import { VsCodeClientWorkspace } from './workspace'; - -interface MdServerInitializationOptions extends LsConfiguration { } - -const organizeLinkDefKind = 'source.organizeLinkDefinitions'; - -export async function startVsCodeServer(connection: Connection) { - const configurationManager = new ConfigurationManager(connection); - const logger = new LogFunctionLogger(connection.console.log.bind(connection.console), configurationManager); - - const parser = new class implements md.IMdParser { - slugifier = md.githubSlugifier; - - tokenize(document: md.ITextDocument): Promise { - return connection.sendRequest(protocol.parse, { uri: document.uri.toString() }); - } - }; - - const documents = new TextDocuments(TextDocument); - const notebooks = new NotebookDocuments(documents); - - const workspaceFactory: WorkspaceFactory = ({ connection, config, workspaceFolders }) => { - const workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks, logger); - workspace.workspaceFolders = (workspaceFolders ?? []).map(x => URI.parse(x.uri)); - return workspace; - }; - - return startServer(connection, { documents, notebooks, configurationManager, logger, parser, workspaceFactory }); -} - -type WorkspaceFactory = (config: { - connection: Connection; - config: LsConfiguration; - workspaceFolders?: lsp.WorkspaceFolder[] | null; -}) => md.IWorkspace; - -export async function startServer(connection: Connection, serverConfig: { - documents: TextDocuments; - notebooks?: NotebookDocuments; - configurationManager: ConfigurationManager; - logger: md.ILogger; - parser: md.IMdParser; - workspaceFactory: WorkspaceFactory; -}) { - const { documents, notebooks } = serverConfig; - - let mdLs: md.IMdLanguageService | undefined; - - connection.onInitialize((params: InitializeParams): InitializeResult => { - const initOptions = params.initializationOptions as MdServerInitializationOptions | undefined; - - const mdConfig = getLsConfiguration(initOptions ?? {}); - - const workspace = serverConfig.workspaceFactory({ connection, config: mdConfig, workspaceFolders: params.workspaceFolders }); - mdLs = md.createLanguageService({ - workspace, - parser: serverConfig.parser, - logger: serverConfig.logger, - ...mdConfig, - get preferredMdPathExtensionStyle() { - switch (serverConfig.configurationManager.getSettings()?.markdown.preferredMdPathExtensionStyle) { - case 'includeExtension': return md.PreferredMdPathExtensionStyle.includeExtension; - case 'removeExtension': return md.PreferredMdPathExtensionStyle.removeExtension; - case 'auto': - default: - return md.PreferredMdPathExtensionStyle.auto; - } - } - }); - - registerCompletionsSupport(connection, documents, mdLs, serverConfig.configurationManager); - registerDocumentHighlightSupport(connection, documents, mdLs, serverConfig.configurationManager); - registerValidateSupport(connection, workspace, documents, mdLs, serverConfig.configurationManager, serverConfig.logger); - - return { - capabilities: { - diagnosticProvider: { - documentSelector: null, - identifier: 'markdown', - interFileDependencies: true, - workspaceDiagnostics: false, - }, - codeActionProvider: { - resolveProvider: true, - codeActionKinds: [ - organizeLinkDefKind, - 'quickfix', - 'refactor', - ] - }, - definitionProvider: true, - documentLinkProvider: { resolveProvider: true }, - documentSymbolProvider: true, - foldingRangeProvider: true, - referencesProvider: true, - renameProvider: { prepareProvider: true, }, - selectionRangeProvider: true, - workspaceSymbolProvider: true, - workspace: { - workspaceFolders: { - supported: true, - changeNotifications: true, - }, - } - } - }; - }); - - connection.onDocumentLinks(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getDocumentLinks(document, token); - }); - - connection.onDocumentLinkResolve(async (link, token): Promise => { - return mdLs!.resolveDocumentLink(link, token); - }); - - connection.onDocumentSymbol(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getDocumentSymbols(document, { includeLinkDefinitions: true }, token); - }); - - connection.onFoldingRanges(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getFoldingRanges(document, token); - }); - - connection.onSelectionRanges(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getSelectionRanges(document, params.positions, token); - }); - - connection.onWorkspaceSymbol(async (params, token): Promise => { - return mdLs!.getWorkspaceSymbols(params.query, token); - }); - - connection.onReferences(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return []; - } - return mdLs!.getReferences(document, params.position, params.context, token); - }); - - connection.onDefinition(async (params, token): Promise => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - return mdLs!.getDefinition(document, params.position, token); - }); - - connection.onPrepareRename(async (params, token) => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - - try { - return await mdLs!.prepareRename(document, params.position, token); - } catch (e) { - if (e instanceof md.RenameNotSupportedAtLocationError) { - throw new ResponseError(0, e.message); - } else { - throw e; - } - } - }); - - connection.onRenameRequest(async (params, token) => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - return mdLs!.getRenameEdit(document, params.position, params.newName, token); - }); - - interface OrganizeLinkActionData { - readonly uri: string; - } - - connection.onCodeAction(async (params, token) => { - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - - if (params.context.only?.some(kind => kind === 'source' || kind.startsWith('source.'))) { - const action: lsp.CodeAction = { - title: l10n.t("Organize link definitions"), - kind: organizeLinkDefKind, - data: { uri: document.uri } satisfies OrganizeLinkActionData, - }; - return [action]; - } - - return mdLs!.getCodeActions(document, params.range, params.context, token); - }); - - connection.onCodeActionResolve(async (codeAction, token) => { - if (codeAction.kind === organizeLinkDefKind) { - const data = codeAction.data as OrganizeLinkActionData; - const document = documents.get(data.uri); - if (!document) { - return codeAction; - } - - const edits = (await mdLs?.organizeLinkDefinitions(document, { removeUnused: true }, token)) || []; - codeAction.edit = { - changes: { - [data.uri]: edits - } - }; - return codeAction; - } - - return codeAction; - }); - - connection.onRequest(protocol.getReferencesToFileInWorkspace, (async (params: { uri: string }, token: CancellationToken) => { - return mdLs!.getFileReferences(URI.parse(params.uri), token); - })); - - connection.onRequest(protocol.getEditForFileRenames, (async (params, token: CancellationToken) => { - const result = await mdLs!.getRenameFilesInWorkspaceEdit(params.map(x => ({ oldUri: URI.parse(x.oldUri), newUri: URI.parse(x.newUri) })), token); - if (!result) { - return result; - } - - return { - edit: result.edit, - participatingRenames: result.participatingRenames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })) - }; - })); - - connection.onRequest(protocol.resolveLinkTarget, (async (params, token: CancellationToken) => { - return mdLs!.resolveLinkTarget(params.linkText, URI.parse(params.uri), token); - })); - - documents.listen(connection); - notebooks?.listen(connection); - connection.listen(); -} - -function registerDynamicClientFeature( - config: ConfigurationManager, - isEnabled: (settings: Settings | undefined) => boolean, - register: () => Promise, -) { - let registration: Promise | undefined; - function update() { - const settings = config.getSettings(); - if (isEnabled(settings)) { - if (!registration) { - registration = register(); - } - } else { - registration?.then(x => x.dispose()); - registration = undefined; - } - } - - update(); - return config.onDidChangeConfiguration(() => update()); -} - -function registerCompletionsSupport( - connection: Connection, - documents: TextDocuments, - ls: md.IMdLanguageService, - config: ConfigurationManager, -): IDisposable { - function getIncludeWorkspaceHeaderCompletions(): md.IncludeWorkspaceHeaderCompletions { - switch (config.getSettings()?.markdown.suggest.paths.includeWorkspaceHeaderCompletions) { - case 'onSingleOrDoubleHash': return md.IncludeWorkspaceHeaderCompletions.onSingleOrDoubleHash; - case 'onDoubleHash': return md.IncludeWorkspaceHeaderCompletions.onDoubleHash; - case 'never': - default: return md.IncludeWorkspaceHeaderCompletions.never; - } - } - - connection.onCompletion(async (params, token): Promise => { - const settings = config.getSettings(); - if (!settings?.markdown.suggest.paths.enabled) { - return []; - } - - const document = documents.get(params.textDocument.uri); - if (document) { - // TODO: remove any type after picking up new release with correct types - return ls.getCompletionItems(document, params.position, { - ...(params.context || {}), - includeWorkspaceHeaderCompletions: getIncludeWorkspaceHeaderCompletions(), - } as any, token); - } - return []; - }); - - return registerDynamicClientFeature(config, (settings) => !!settings?.markdown.suggest.paths.enabled, () => { - const registrationOptions: CompletionRegistrationOptions = { - documentSelector: null, - triggerCharacters: ['.', '/', '#'], - }; - return connection.client.register(CompletionRequest.type, registrationOptions); - }); -} - -function registerDocumentHighlightSupport( - connection: Connection, - documents: TextDocuments, - mdLs: md.IMdLanguageService, - configurationManager: ConfigurationManager -) { - connection.onDocumentHighlight(async (params, token) => { - const settings = configurationManager.getSettings(); - if (!settings?.markdown.occurrencesHighlight.enabled) { - return undefined; - } - - const document = documents.get(params.textDocument.uri); - if (!document) { - return undefined; - } - - return mdLs!.getDocumentHighlights(document, params.position, token); - }); - - return registerDynamicClientFeature(configurationManager, (settings) => !!settings?.markdown.occurrencesHighlight.enabled, () => { - const registrationOptions: DocumentHighlightRegistrationOptions = { - documentSelector: null, - }; - return connection.client.register(DocumentHighlightRequest.type, registrationOptions); - }); -} diff --git a/extensions/markdown-language-features/server/src/util/dispose.ts b/extensions/markdown-language-features/server/src/util/dispose.ts deleted file mode 100644 index 2e9d8dc6a14..00000000000 --- a/extensions/markdown-language-features/server/src/util/dispose.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export function disposeAll(disposables: Iterable) { - const errors: any[] = []; - - for (const disposable of disposables) { - try { - disposable.dispose(); - } catch (e) { - errors.push(e); - } - } - - if (errors.length === 1) { - throw errors[0]; - } else if (errors.length > 1) { - throw new AggregateError(errors, 'Encountered errors while disposing of store'); - } -} - -export interface IDisposable { - dispose(): void; -} - -export abstract class Disposable { - private _isDisposed = false; - - protected _disposables: IDisposable[] = []; - - public dispose(): any { - if (this._isDisposed) { - return; - } - this._isDisposed = true; - disposeAll(this._disposables); - } - - protected _register(value: T): T { - if (this._isDisposed) { - value.dispose(); - } else { - this._disposables.push(value); - } - return value; - } - - protected get isDisposed() { - return this._isDisposed; - } -} - diff --git a/extensions/markdown-language-features/server/src/util/file.ts b/extensions/markdown-language-features/server/src/util/file.ts deleted file mode 100644 index 10e95bf5dcf..00000000000 --- a/extensions/markdown-language-features/server/src/util/file.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 { TextDocument } from 'vscode-languageserver-textdocument'; -import { URI, Utils } from 'vscode-uri'; -import { LsConfiguration } from '../config'; - -export function looksLikeMarkdownPath(config: LsConfiguration, resolvedHrefPath: URI) { - return config.markdownFileExtensions.includes(Utils.extname(resolvedHrefPath).toLowerCase().replace('.', '')); -} - -export function isMarkdownFile(document: TextDocument) { - return document.languageId === 'markdown'; -} diff --git a/extensions/markdown-language-features/server/src/util/limiter.ts b/extensions/markdown-language-features/server/src/util/limiter.ts deleted file mode 100644 index bd4153cd08b..00000000000 --- a/extensions/markdown-language-features/server/src/util/limiter.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -interface ILimitedTaskFactory { - factory: ITask>; - c: (value: T | Promise) => void; - e: (error?: unknown) => void; -} - -interface ITask { - (): T; -} - -/** - * A helper to queue N promises and run them all with a max degree of parallelism. The helper - * ensures that at any time no more than M promises are running at the same time. - * - * Taken from 'src/vs/base/common/async.ts' - */ -export class Limiter { - - private _size = 0; - private runningPromises: number; - private readonly maxDegreeOfParalellism: number; - private readonly outstandingPromises: ILimitedTaskFactory[]; - - constructor(maxDegreeOfParalellism: number) { - this.maxDegreeOfParalellism = maxDegreeOfParalellism; - this.outstandingPromises = []; - this.runningPromises = 0; - } - - get size(): number { - return this._size; - } - - queue(factory: ITask>): Promise { - this._size++; - - return new Promise((c, e) => { - this.outstandingPromises.push({ factory, c, e }); - this.consume(); - }); - } - - private consume(): void { - while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { - const iLimitedTask = this.outstandingPromises.shift()!; - this.runningPromises++; - - const promise = iLimitedTask.factory(); - promise.then(iLimitedTask.c, iLimitedTask.e); - promise.then(() => this.consumed(), () => this.consumed()); - } - } - - private consumed(): void { - this._size--; - this.runningPromises--; - - if (this.outstandingPromises.length > 0) { - this.consume(); - } - } -} diff --git a/extensions/markdown-language-features/server/src/util/resourceMap.ts b/extensions/markdown-language-features/server/src/util/resourceMap.ts deleted file mode 100644 index 7cec9d661d3..00000000000 --- a/extensions/markdown-language-features/server/src/util/resourceMap.ts +++ /dev/null @@ -1,69 +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 { URI } from 'vscode-uri'; - - -type ResourceToKey = (uri: URI) => string; - -const defaultResourceToKey = (resource: URI): string => resource.toString(); - -export class ResourceMap { - - private readonly map = new Map(); - - private readonly toKey: ResourceToKey; - - constructor(toKey: ResourceToKey = defaultResourceToKey) { - this.toKey = toKey; - } - - public set(uri: URI, value: T): this { - this.map.set(this.toKey(uri), { uri, value }); - return this; - } - - public get(resource: URI): T | undefined { - return this.map.get(this.toKey(resource))?.value; - } - - public has(resource: URI): boolean { - return this.map.has(this.toKey(resource)); - } - - public get size(): number { - return this.map.size; - } - - public clear(): void { - this.map.clear(); - } - - public delete(resource: URI): boolean { - return this.map.delete(this.toKey(resource)); - } - - public *values(): IterableIterator { - for (const entry of this.map.values()) { - yield entry.value; - } - } - - public *keys(): IterableIterator { - for (const entry of this.map.values()) { - yield entry.uri; - } - } - - public *entries(): IterableIterator<[URI, T]> { - for (const entry of this.map.values()) { - yield [entry.uri, entry.value]; - } - } - - public [Symbol.iterator](): IterableIterator<[URI, T]> { - return this.entries(); - } -} diff --git a/extensions/markdown-language-features/server/src/workspace.ts b/extensions/markdown-language-features/server/src/workspace.ts deleted file mode 100644 index b1bf87c3020..00000000000 --- a/extensions/markdown-language-features/server/src/workspace.ts +++ /dev/null @@ -1,421 +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 { Connection, Emitter, FileChangeType, NotebookDocuments, Position, Range, TextDocuments } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import * as md from 'vscode-markdown-languageservice'; -import { URI } from 'vscode-uri'; -import { LsConfiguration } from './config'; -import * as protocol from './protocol'; -import { isMarkdownFile, looksLikeMarkdownPath } from './util/file'; -import { Limiter } from './util/limiter'; -import { ResourceMap } from './util/resourceMap'; -import { Schemes } from './util/schemes'; - -declare const TextDecoder: any; - -class VsCodeDocument implements md.ITextDocument { - - private inMemoryDoc?: TextDocument; - private onDiskDoc?: TextDocument; - - readonly uri: string; - - constructor(uri: string, init: { inMemoryDoc: TextDocument }); - constructor(uri: string, init: { onDiskDoc: TextDocument }); - constructor(uri: string, init: { inMemoryDoc?: TextDocument; onDiskDoc?: TextDocument }) { - this.uri = uri; - this.inMemoryDoc = init?.inMemoryDoc; - this.onDiskDoc = init?.onDiskDoc; - } - - get version(): number { - return this.inMemoryDoc?.version ?? this.onDiskDoc?.version ?? 0; - } - - get lineCount(): number { - return this.inMemoryDoc?.lineCount ?? this.onDiskDoc?.lineCount ?? 0; - } - - getText(range?: Range): string { - if (this.inMemoryDoc) { - return this.inMemoryDoc.getText(range); - } - - if (this.onDiskDoc) { - return this.onDiskDoc.getText(range); - } - - throw new Error('Document has been closed'); - } - - positionAt(offset: number): Position { - if (this.inMemoryDoc) { - return this.inMemoryDoc.positionAt(offset); - } - - if (this.onDiskDoc) { - return this.onDiskDoc.positionAt(offset); - } - - throw new Error('Document has been closed'); - } - - hasInMemoryDoc(): boolean { - return !!this.inMemoryDoc; - } - - isDetached(): boolean { - return !this.onDiskDoc && !this.inMemoryDoc; - } - - setInMemoryDoc(doc: TextDocument | undefined) { - this.inMemoryDoc = doc; - } - - setOnDiskDoc(doc: TextDocument | undefined) { - this.onDiskDoc = doc; - } -} - -export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { - - private readonly _onDidCreateMarkdownDocument = new Emitter(); - public readonly onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocument.event; - - private readonly _onDidChangeMarkdownDocument = new Emitter(); - public readonly onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocument.event; - - private readonly _onDidDeleteMarkdownDocument = new Emitter(); - public readonly onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocument.event; - - private readonly _documentCache = new ResourceMap(); - - private readonly _utf8Decoder = new TextDecoder('utf-8'); - - private _watcherPool = 0; - private readonly _watchers = new Map; - readonly onDidCreate: Emitter; - readonly onDidDelete: Emitter; - }>(); - - constructor( - private readonly connection: Connection, - private readonly config: LsConfiguration, - private readonly documents: TextDocuments, - private readonly notebooks: NotebookDocuments, - private readonly logger: md.ILogger, - ) { - documents.onDidOpen(e => { - if (!this.isRelevantMarkdownDocument(e.document)) { - return; - } - - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidOpen', { document: e.document.uri }); - - const uri = URI.parse(e.document.uri); - const doc = this._documentCache.get(uri); - - if (doc) { - // File already existed on disk - doc.setInMemoryDoc(e.document); - - // The content visible to the language service may have changed since the in-memory doc - // may differ from the one on-disk. To be safe we always fire a change event. - this._onDidChangeMarkdownDocument.fire(doc); - } else { - // We're creating the file for the first time - const doc = new VsCodeDocument(e.document.uri, { inMemoryDoc: e.document }); - this._documentCache.set(uri, doc); - this._onDidCreateMarkdownDocument.fire(doc); - } - }); - - documents.onDidChangeContent(e => { - if (!this.isRelevantMarkdownDocument(e.document)) { - return; - } - - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidChanceContent', { document: e.document.uri }); - - const uri = URI.parse(e.document.uri); - const entry = this._documentCache.get(uri); - if (entry) { - entry.setInMemoryDoc(e.document); - this._onDidChangeMarkdownDocument.fire(entry); - } - }); - - documents.onDidClose(async e => { - if (!this.isRelevantMarkdownDocument(e.document)) { - return; - } - - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidClose', { document: e.document.uri }); - - const uri = URI.parse(e.document.uri); - const doc = this._documentCache.get(uri); - if (!doc) { - // Document was never opened - return; - } - - doc.setInMemoryDoc(undefined); - if (doc.isDetached()) { - // The document has been fully closed - this.doDeleteDocument(uri); - return; - } - - // Check that if file has been deleted on disk. - // This can happen when directories are renamed / moved. VS Code's file system watcher does not - // notify us when this happens. - if (!(await this.statBypassingCache(uri))) { - if (this._documentCache.get(uri) === doc && !doc.hasInMemoryDoc()) { - this.doDeleteDocument(uri); - return; - } - } - - // The document still exists on disk - // To be safe, tell the service that the document has changed because the - // in-memory doc contents may be different than the disk doc contents. - this._onDidChangeMarkdownDocument.fire(doc); - }); - - connection.onDidChangeWatchedFiles(async ({ changes }) => { - for (const change of changes) { - const resource = URI.parse(change.uri); - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.onDidChangeWatchedFiles', { type: change.type, resource: resource.toString() }); - switch (change.type) { - case FileChangeType.Changed: { - const entry = this._documentCache.get(resource); - if (entry) { - // Refresh the on-disk state - const document = await this.openMarkdownDocumentFromFs(resource); - if (document) { - this._onDidChangeMarkdownDocument.fire(document); - } - } - break; - } - case FileChangeType.Created: { - const entry = this._documentCache.get(resource); - if (entry) { - // Create or update the on-disk state - const document = await this.openMarkdownDocumentFromFs(resource); - if (document) { - this._onDidCreateMarkdownDocument.fire(document); - } - } - break; - } - case FileChangeType.Deleted: { - const entry = this._documentCache.get(resource); - if (entry) { - entry.setOnDiskDoc(undefined); - if (entry.isDetached()) { - this.doDeleteDocument(resource); - } - } - break; - } - } - } - }); - - connection.onRequest(protocol.fs_watcher_onChange, params => { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.fs_watcher_onChange', { kind: params.kind, uri: params.uri }); - - const watcher = this._watchers.get(params.id); - if (!watcher) { - return; - } - - switch (params.kind) { - case 'create': watcher.onDidCreate.fire(URI.parse(params.uri)); return; - case 'change': watcher.onDidChange.fire(URI.parse(params.uri)); return; - case 'delete': watcher.onDidDelete.fire(URI.parse(params.uri)); return; - } - }); - } - - public listen() { - this.connection.workspace.onDidChangeWorkspaceFolders(async () => { - this.workspaceFolders = (await this.connection.workspace.getWorkspaceFolders() ?? []).map(x => URI.parse(x.uri)); - }); - } - - private _workspaceFolders: readonly URI[] = []; - - get workspaceFolders(): readonly URI[] { - return this._workspaceFolders; - } - - set workspaceFolders(value: readonly URI[]) { - this._workspaceFolders = value; - } - - async getAllMarkdownDocuments(): Promise> { - // Add opened files (such as untitled files) - const openTextDocumentResults = this.documents.all() - .filter(doc => this.isRelevantMarkdownDocument(doc)); - - const allDocs = new ResourceMap(); - for (const doc of openTextDocumentResults) { - allDocs.set(URI.parse(doc.uri), doc); - } - - // And then add files on disk - const maxConcurrent = 20; - const limiter = new Limiter(maxConcurrent); - const resources = await this.connection.sendRequest(protocol.findMarkdownFilesInWorkspace, {}); - await Promise.all(resources.map(strResource => { - return limiter.queue(async () => { - const resource = URI.parse(strResource); - if (allDocs.has(resource)) { - return; - } - - const doc = await this.openMarkdownDocument(resource); - if (doc) { - allDocs.set(resource, doc); - } - return doc; - }); - })); - - return allDocs.values(); - } - - hasMarkdownDocument(resource: URI): boolean { - return !!this.documents.get(resource.toString()); - } - - async openMarkdownDocument(resource: URI): Promise { - const existing = this._documentCache.get(resource); - if (existing) { - return existing; - } - - const matchingDocument = this.documents.get(resource.toString()); - if (matchingDocument) { - let entry = this._documentCache.get(resource); - if (entry) { - entry.setInMemoryDoc(matchingDocument); - } else { - entry = new VsCodeDocument(resource.toString(), { inMemoryDoc: matchingDocument }); - this._documentCache.set(resource, entry); - } - - return entry; - } - - return this.openMarkdownDocumentFromFs(resource); - } - - private async openMarkdownDocumentFromFs(resource: URI): Promise { - if (!looksLikeMarkdownPath(this.config, resource)) { - return undefined; - } - - try { - const response = await this.connection.sendRequest(protocol.fs_readFile, { uri: resource.toString() }); - // TODO: LSP doesn't seem to handle Array buffers well - const bytes = new Uint8Array(response); - - // We assume that markdown is in UTF-8 - const text = this._utf8Decoder.decode(bytes); - const doc = new VsCodeDocument(resource.toString(), { - onDiskDoc: TextDocument.create(resource.toString(), 'markdown', 0, text) - }); - this._documentCache.set(resource, doc); - return doc; - } catch (e) { - return undefined; - } - } - - async stat(resource: URI): Promise { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.stat', { resource: resource.toString() }); - if (this._documentCache.has(resource)) { - return { isDirectory: false }; - } - return this.statBypassingCache(resource); - } - - private async statBypassingCache(resource: URI): Promise { - const uri = resource.toString(); - if (this.documents.get(uri)) { - return { isDirectory: false }; - } - const fsResult = await this.connection.sendRequest(protocol.fs_stat, { uri }); - return fsResult ?? undefined; // Force convert null to undefined - } - - async readDirectory(resource: URI): Promise<[string, md.FileStat][]> { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.readDir', { resource: resource.toString() }); - return this.connection.sendRequest(protocol.fs_readDirectory, { uri: resource.toString() }); - } - - getContainingDocument(resource: URI): md.ContainingDocumentContext | undefined { - if (resource.scheme === Schemes.notebookCell) { - const nb = this.notebooks.findNotebookDocumentForCell(resource.toString()); - if (nb) { - return { - uri: URI.parse(nb.uri), - children: nb.cells.map(cell => ({ uri: URI.parse(cell.document) })), - }; - } - } - return undefined; - } - - watchFile(resource: URI, options: md.FileWatcherOptions): md.IFileSystemWatcher { - const id = this._watcherPool++; - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.watchFile', { id, resource: resource.toString() }); - - const entry = { - resource, - options, - onDidCreate: new Emitter(), - onDidChange: new Emitter(), - onDidDelete: new Emitter(), - }; - this._watchers.set(id, entry); - - this.connection.sendRequest(protocol.fs_watcher_create, { - id, - uri: resource.toString(), - options, - watchParentDirs: true, - }); - - return { - onDidCreate: entry.onDidCreate.event, - onDidChange: entry.onDidChange.event, - onDidDelete: entry.onDidDelete.event, - dispose: () => { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.disposeWatcher', { id, resource: resource.toString() }); - this.connection.sendRequest(protocol.fs_watcher_delete, { id }); - this._watchers.delete(id); - } - }; - } - - private isRelevantMarkdownDocument(doc: TextDocument) { - return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview'; - } - - private doDeleteDocument(uri: URI) { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.deleteDocument', { document: uri.toString() }); - - this._documentCache.delete(uri); - this._onDidDeleteMarkdownDocument.fire(uri); - } -} diff --git a/extensions/markdown-language-features/server/tsconfig.json b/extensions/markdown-language-features/server/tsconfig.json deleted file mode 100644 index 0a73af08ed8..00000000000 --- a/extensions/markdown-language-features/server/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./out", - "lib": [ - "ES2020", - "ES2021.Promise", - "WebWorker" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock deleted file mode 100644 index d630fdb5e88..00000000000 --- a/extensions/markdown-language-features/server/yarn.lock +++ /dev/null @@ -1,163 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== - -"@vscode/l10n@^0.0.10": - version "0.0.10" - resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.10.tgz#9c513107c690c0dd16e3ec61e453743de15ebdb0" - integrity sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ== - -"@vscode/l10n@^0.0.11": - version "0.0.11" - resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.11.tgz#325d7beb2cfb87162bc624d16c4d546de6a73b72" - integrity sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA== - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - -entities@^4.2.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -node-html-parser@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.5.tgz#c819dceb13a10a7642ff92f94f870b4f77968097" - integrity sha512-fAaM511feX++/Chnhe475a0NHD8M7AxDInsqQpz6x63GRF7xYNdS8Vo5dKsIVPgsOvG7eioRRTZQnWBrhDHBSg== - dependencies: - css-select "^5.1.0" - he "1.2.0" - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -vscode-jsonrpc@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94" - integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw== - -vscode-jsonrpc@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" - integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== - -vscode-languageserver-protocol@3.17.3: - version "3.17.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz#6d0d54da093f0c0ee3060b81612cce0f11060d57" - integrity sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA== - dependencies: - vscode-jsonrpc "8.1.0" - vscode-languageserver-types "3.17.3" - -vscode-languageserver-protocol@^3.17.1: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" - integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== - dependencies: - vscode-jsonrpc "8.2.0" - vscode-languageserver-types "3.17.5" - -vscode-languageserver-textdocument@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" - integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== - -vscode-languageserver-types@3.17.3, vscode-languageserver-types@^3.17.3: - version "3.17.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64" - integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA== - -vscode-languageserver-types@3.17.5: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" - integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== - -vscode-languageserver@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz#5024253718915d84576ce6662dd46a791498d827" - integrity sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw== - dependencies: - vscode-languageserver-protocol "3.17.3" - -vscode-markdown-languageservice@^0.5.0-alpha.1: - version "0.5.0-alpha.1" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.1.tgz#0b03f1f8e853e20352587a8de6a2f10f8d14c81c" - integrity sha512-7uVtRSr4+/xlsml9QtBkBAHPmtBv71CKEj6zhPTERYZEOHCwFsue1EHkESWBOGuqQ1NSLXPnHWENcDh5VD+bNw== - dependencies: - "@vscode/l10n" "^0.0.10" - node-html-parser "^6.1.5" - picomatch "^2.3.1" - vscode-languageserver-protocol "^3.17.1" - vscode-uri "^3.0.7" - -vscode-uri@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" - integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== diff --git a/extensions/markdown-language-features/src/client/client.ts b/extensions/markdown-language-features/src/client/client.ts index f0e925f6157..a2f78615508 100644 --- a/extensions/markdown-language-features/src/client/client.ts +++ b/extensions/markdown-language-features/src/client/client.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient'; +import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType, Range, TextEdit } from 'vscode-languageclient'; import { IMdParser } from '../markdownEngine'; -import * as proto from './protocol'; -import { looksLikeMarkdownPath, markdownFileExtensions } from '../util/file'; -import { VsCodeMdWorkspace } from './workspace'; -import { FileWatcherManager } from './fileWatchingManager'; import { IDisposable } from '../util/dispose'; - +import { looksLikeMarkdownPath, markdownFileExtensions } from '../util/file'; +import { FileWatcherManager } from './fileWatchingManager'; +import { InMemoryDocument } from './inMemoryDocument'; +import * as proto from './protocol'; +import { VsCodeMdWorkspace } from './workspace'; export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; @@ -38,6 +38,21 @@ export class MdLanguageClient implements IDisposable { getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken) { return this._client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token); } + + prepareUpdatePastedLinks(doc: vscode.Uri, ranges: readonly vscode.Range[], token: vscode.CancellationToken) { + return this._client.sendRequest(proto.prepareUpdatePastedLinks, { + uri: doc.toString(), + ranges: ranges.map(range => Range.create(range.start.line, range.start.character, range.end.line, range.end.character)), + }, token); + } + + getUpdatePastedLinksEdit(pastingIntoDoc: vscode.Uri, edits: readonly vscode.TextEdit[], metadata: string, token: vscode.CancellationToken) { + return this._client.sendRequest(proto.getUpdatePastedLinksEdit, { + metadata, + pasteIntoDoc: pastingIntoDoc.toString(), + edits: edits.map(edit => TextEdit.replace(edit.range, edit.newText)), + }, token); + } } export async function startClient(factory: LanguageClientConstructor, parser: IMdParser): Promise { @@ -61,6 +76,9 @@ export async function startClient(factory: LanguageClientConstructor, parser: IM return looksLikeMarkdownPath(resource); }, }, + markdown: { + supportHtml: true, + } }; const client = factory('markdown', vscode.l10n.t("Markdown Language Server"), clientOptions); @@ -84,11 +102,15 @@ export async function startClient(factory: LanguageClientConstructor, parser: IM client.onRequest(proto.parse, async (e) => { const uri = vscode.Uri.parse(e.uri); - const doc = await workspace.getOrLoadMarkdownDocument(uri); - if (doc) { - return parser.tokenize(doc); + if (typeof e.text === 'string') { + return parser.tokenize(new InMemoryDocument(uri, e.text, -1)); } else { - return []; + const doc = await workspace.getOrLoadMarkdownDocument(uri); + if (doc) { + return parser.tokenize(doc); + } else { + return []; + } } }); diff --git a/extensions/markdown-language-features/src/client/protocol.ts b/extensions/markdown-language-features/src/client/protocol.ts index f906460fce9..69d162f8262 100644 --- a/extensions/markdown-language-features/src/client/protocol.ts +++ b/extensions/markdown-language-features/src/client/protocol.ts @@ -16,7 +16,7 @@ export type ResolvedDocumentLinkTarget = | { readonly kind: 'external'; readonly uri: vscode.Uri }; //#region From server -export const parse = new RequestType<{ uri: string }, Token[], any>('markdown/parse'); +export const parse = new RequestType<{ uri: string; text?: string }, Token[], any>('markdown/parse'); export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile'); export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory'); @@ -32,6 +32,9 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>(' export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace'); export const getEditForFileRenames = new RequestType, { participatingRenames: readonly FileRename[]; edit: lsp.WorkspaceEdit }, any>('markdown/getEditForFileRenames'); +export const prepareUpdatePastedLinks = new RequestType<{ uri: string; ranges: lsp.Range[] }, string, any>('markdown/prepareUpdatePastedLinks'); +export const getUpdatePastedLinksEdit = new RequestType<{ pasteIntoDoc: string; metadata: string; edits: lsp.TextEdit[] }, lsp.TextEdit[] | undefined, any>('markdown/getUpdatePastedLinksEdit'); + export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange'); export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget'); diff --git a/extensions/markdown-language-features/src/extension.browser.ts b/extensions/markdown-language-features/src/extension.browser.ts index 30639672490..2bfc63fc857 100644 --- a/extensions/markdown-language-features/src/extension.browser.ts +++ b/extensions/markdown-language-features/src/extension.browser.ts @@ -27,7 +27,7 @@ export async function activate(context: vscode.ExtensionContext) { } function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise { - const serverMain = vscode.Uri.joinPath(context.extensionUri, 'server/dist/browser/workerMain.js'); + const serverMain = vscode.Uri.joinPath(context.extensionUri, 'dist', 'browser', 'serverWorkerMain.js'); const worker = new Worker(serverMain.toString()); worker.postMessage({ i10lLocation: vscode.l10n.uri?.toString() ?? '' }); diff --git a/extensions/markdown-language-features/src/extension.shared.ts b/extensions/markdown-language-features/src/extension.shared.ts index e8758bad832..e062666c748 100644 --- a/extensions/markdown-language-features/src/extension.shared.ts +++ b/extensions/markdown-language-features/src/extension.shared.ts @@ -20,6 +20,7 @@ import { MarkdownPreviewManager } from './preview/previewManager'; import { ExtensionContentSecurityPolicyArbiter } from './preview/security'; import { loadDefaultTelemetryReporter } from './telemetryReporter'; import { MdLinkOpener } from './util/openDocumentLink'; +import { registerUpdatePastedLinks } from './languageFeatures/updateLinksOnPaste'; export function activateShared( context: vscode.ExtensionContext, @@ -61,5 +62,6 @@ function registerMarkdownLanguageFeatures( registerResourceDropOrPasteSupport(selector, parser), registerPasteUrlSupport(selector, parser), registerUpdateLinksOnRename(client), + registerUpdatePastedLinks(selector, client), ); } diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index b14ab6d0e7e..98ea87df069 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -27,10 +27,15 @@ export async function activate(context: vscode.ExtensionContext) { } function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise { - const clientMain = vscode.extensions.getExtension('vscode.markdown-language-features')?.packageJSON?.main || ''; + const isDebugBuild = context.extension.packageJSON.main.includes('/out/'); - const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/workerMain`; - const serverModule = context.asAbsolutePath(serverMain); + const serverModule = context.asAbsolutePath( + isDebugBuild + // For local non bundled version of vscode-markdown-languageserver + // ? './node_modules/vscode-markdown-languageserver/out/node/workerMain' + ? './node_modules/vscode-markdown-languageserver/dist/node/workerMain' + : './dist/serverWorkerMain' + ); // The debug options for the server const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (7000 + Math.round(Math.random() * 999))] }; diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts index 73a012dacec..9fd3e4f388d 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts @@ -30,7 +30,7 @@ enum CopyFilesSettings { */ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider { - public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'link'); + public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link'); public static readonly mimeTypes = [ Mime.textUriList, @@ -39,8 +39,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v ]; private readonly _yieldTo = [ - vscode.DocumentPasteEditKind.Empty.append('text'), - vscode.DocumentPasteEditKind.Empty.append('markdown', 'image', 'attachment'), + vscode.DocumentDropOrPasteEditKind.Empty.append('text'), + vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'image', 'attachment'), ]; constructor( @@ -133,7 +133,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v } if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) { - edit.yieldTo.push(vscode.DocumentPasteEditKind.Empty.append('uri')); + edit.yieldTo.push(vscode.DocumentDropOrPasteEditKind.Empty.append('uri')); } return edit; @@ -156,9 +156,14 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v return; } - // Disable ourselves if there's also a text entry with the same content as our list, + // In some browsers, copying from the address bar sets both text/uri-list and text/plain. + // Disable ourselves if there's also a text entry with the same http(s) uri as our list, // unless we are explicitly requested. - if (uriList.entries.length === 1 && !context?.only?.contains(ResourcePasteOrDropProvider.kind)) { + if ( + uriList.entries.length === 1 + && (uriList.entries[0].uri.scheme === Schemes.http || uriList.entries[0].uri.scheme === Schemes.https) + && !context?.only?.contains(ResourcePasteOrDropProvider.kind) + ) { const text = await dataTransfer.get(Mime.textPlain)?.asString(); if (token.isCancellationRequested) { return; diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index 7fcff576a3d..661b0bfd05f 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts @@ -17,7 +17,7 @@ import { UriList } from '../../util/uriList'; */ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { - public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('markdown', 'link'); + public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link'); public static readonly pasteMimeTypes = [Mime.textPlain]; @@ -61,8 +61,8 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, pasteUrlSetting, ranges, token))) { pasteEdit.yieldTo = [ - vscode.DocumentPasteEditKind.Empty.append('text'), - vscode.DocumentPasteEditKind.Empty.append('uri') + vscode.DocumentDropOrPasteEditKind.Empty.append('text'), + vscode.DocumentDropOrPasteEditKind.Empty.append('uri') ]; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index 563c125cfc6..4ab245c192b 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -20,6 +20,7 @@ enum MediaKind { export const mediaFileExtensions = new Map([ // Images + ['avif', MediaKind.Image], ['bmp', MediaKind.Image], ['gif', MediaKind.Image], ['ico', MediaKind.Image], @@ -74,7 +75,6 @@ export function createInsertUriListEdit( return; } - const edits: vscode.SnippetTextEdit[] = []; let insertedLinkCount = 0; @@ -266,5 +266,5 @@ export interface DropOrPasteEdit { readonly snippet: vscode.SnippetString; readonly label: string; readonly additionalEdits: vscode.WorkspaceEdit; - readonly yieldTo: vscode.DocumentPasteEditKind[]; + readonly yieldTo: vscode.DocumentDropOrPasteEditKind[]; } diff --git a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts index 2f2af15df08..bda8b721e8b 100644 --- a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts +++ b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts @@ -28,7 +28,7 @@ export class FindFileReferencesCommand implements Command { location: vscode.ProgressLocation.Window, title: vscode.l10n.t("Finding file references") }, async (_progress, token) => { - const locations = (await this._client.getReferencesToFileInWorkspace(resource!, token)).map(loc => { + const locations = (await this._client.getReferencesToFileInWorkspace(resource, token)).map(loc => { return new vscode.Location(vscode.Uri.parse(loc.uri), convertRange(loc.range)); }); diff --git a/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts new file mode 100644 index 00000000000..c8ad4c722fd --- /dev/null +++ b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { MdLanguageClient } from '../client/client'; +import { Mime } from '../util/mimes'; + +class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider { + + public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'updateLinks'); + + public static readonly metadataMime = 'vnd.vscode.markdown.updateLinksMetadata'; + + constructor( + private readonly _client: MdLanguageClient, + ) { } + + async prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + if (!this._isEnabled(document)) { + return; + } + + const metadata = await this._client.prepareUpdatePastedLinks(document.uri, ranges, token); + if (token.isCancellationRequested) { + return; + } + + dataTransfer.set(UpdatePastedLinksEditProvider.metadataMime, new vscode.DataTransferItem(metadata)); + } + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + ranges: readonly vscode.Range[], + dataTransfer: vscode.DataTransfer, + context: vscode.DocumentPasteEditContext, + token: vscode.CancellationToken, + ): Promise { + if (!this._isEnabled(document)) { + return; + } + + const metadata = dataTransfer.get(UpdatePastedLinksEditProvider.metadataMime)?.value; + if (!metadata) { + return; + } + + const textItem = dataTransfer.get(Mime.textPlain); + const text = await textItem?.asString(); + if (!text || token.isCancellationRequested) { + return; + } + + // TODO: Handle cases such as: + // - copy empty line + // - Copy with multiple cursors and paste into multiple locations + // - ... + const edits = await this._client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token); + if (!edits?.length || token.isCancellationRequested) { + return; + } + + const pasteEdit = new vscode.DocumentPasteEdit('', vscode.l10n.t("Paste and update pasted links"), UpdatePastedLinksEditProvider.kind); + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(document.uri, edits.map(x => new vscode.TextEdit(new vscode.Range(x.range.start.line, x.range.start.character, x.range.end.line, x.range.end.character,), x.newText))); + pasteEdit.additionalEdit = workspaceEdit; + + if (!context.only || !UpdatePastedLinksEditProvider.kind.contains(context.only)) { + pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text')]; + } + + return [pasteEdit]; + } + + private _isEnabled(document: vscode.TextDocument): boolean { + return vscode.workspace.getConfiguration('markdown', document.uri).get('editor.updateLinksOnPaste.enabled', true); + } +} + +export function registerUpdatePastedLinks(selector: vscode.DocumentSelector, client: MdLanguageClient) { + return vscode.languages.registerDocumentPasteEditProvider(selector, new UpdatePastedLinksEditProvider(client), { + copyMimeTypes: [UpdatePastedLinksEditProvider.metadataMime], + providedPasteEditKinds: [UpdatePastedLinksEditProvider.kind], + pasteMimeTypes: [UpdatePastedLinksEditProvider.metadataMime], + }); +} diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 0ef05452e4f..5f6e746a82d 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -55,7 +55,7 @@ class TokenCache { public tryGetCached(document: ITextDocument, config: MarkdownItConfig): Token[] | undefined { if (this._cachedDocument && this._cachedDocument.uri.toString() === document.uri.toString() - && this._cachedDocument.version === document.version + && document.version >= 0 && this._cachedDocument.version === document.version && this._cachedDocument.config.breaks === config.breaks && this._cachedDocument.config.linkify === config.linkify ) { @@ -313,7 +313,7 @@ export class MarkdownItEngine implements IMdParser { private _addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; md.renderer.rules.heading_open = (tokens: Token[], idx: number, options, env, self) => { - const title = tokens[idx + 1].children!.reduce((acc, t) => acc + t.content, ''); + const title = this._tokenToPlainText(tokens[idx + 1]); let slug = this.slugifier.fromHeading(title); if (this._slugCount.has(slug.value)) { @@ -334,6 +334,21 @@ export class MarkdownItEngine implements IMdParser { }; } + private _tokenToPlainText(token: Token): string { + if (token.children) { + return token.children.map(x => this._tokenToPlainText(x)).join(''); + } + + switch (token.type) { + case 'text': + case 'emoji': + case 'code_inline': + return token.content; + default: + return ''; + } + } + private _addLinkRenderer(md: MarkdownIt): void { const original = md.renderer.rules.link_open; diff --git a/extensions/markdown-language-features/src/test/pasteUrl.test.ts b/extensions/markdown-language-features/src/test/pasteUrl.test.ts index 2afa4465f76..ea4a3f868af 100644 --- a/extensions/markdown-language-features/src/test/pasteUrl.test.ts +++ b/extensions/markdown-language-features/src/test/pasteUrl.test.ts @@ -11,6 +11,7 @@ import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefau import { noopToken } from '../util/cancellation'; import { UriList } from '../util/uriList'; import { createNewMarkdownEngine } from './engine'; +import { joinLines } from './util'; function makeTestDoc(contents: string) { return new InMemoryDocument(vscode.Uri.file('test.md'), contents); @@ -307,5 +308,36 @@ suite('createEditAddingLinksForUriList', () => { await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<>'), InsertMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken), false); }); + + test('Smart should be disabled in frontmatter', async () => { + const textDoc = makeTestDoc(joinLines( + `---`, + `layout: post`, + `title: Blogging Like a Hacker`, + `---`, + ``, + `Link Text` + )); + assert.strictEqual( + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), textDoc, InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken), + false); + + assert.strictEqual( + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), textDoc, InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken), + false); + }); + + test('Smart should enabled after frontmatter', async () => { + assert.strictEqual( + await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(joinLines( + `---`, + `layout: post`, + `title: Blogging Like a Hacker`, + `---`, + ``, + `Link Text` + )), InsertMarkdownLink.Smart, [new vscode.Range(5, 0, 5, 0)], noopToken), + true); + }); }); }); diff --git a/extensions/markdown-language-features/src/util/mimes.ts b/extensions/markdown-language-features/src/util/mimes.ts index 8028294b3f4..f33b807b83e 100644 --- a/extensions/markdown-language-features/src/util/mimes.ts +++ b/extensions/markdown-language-features/src/util/mimes.ts @@ -9,6 +9,7 @@ export const Mime = { } as const; export const mediaMimes = new Set([ + 'image/avif', 'image/bmp', 'image/gif', 'image/jpeg', diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index 6bbe1c80767..75edc8fdacf 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -6,7 +6,6 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts", - "../../src/vscode-dts/vscode.proposed.dropMetadata.d.ts" + "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts" ] } diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 7ff0968e5af..525795036ab 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -166,6 +166,11 @@ resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.10.tgz#9c513107c690c0dd16e3ec61e453743de15ebdb0" integrity sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ== +"@vscode/l10n@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.11.tgz#325d7beb2cfb87162bc624d16c4d546de6a73b72" + integrity sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA== + "@vscode/markdown-it-katex@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@vscode/markdown-it-katex/-/markdown-it-katex-1.0.2.tgz#27ba579fa3896b2944b71209dd30d0f983983f11" @@ -183,6 +188,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -201,25 +211,81 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + dompurify@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.5.tgz#eb3d9cfa10037b6e73f32c586682c4b2ab01fbed" integrity sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A== +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + entities@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + highlight.js@^11.8.0: version "11.8.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.8.0.tgz#966518ea83257bae2e7c9a48596231856555bb65" integrity sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg== katex@^0.16.4: - version "0.16.9" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.9.tgz#bc62d8f7abfea6e181250f85a56e4ef292dcb1fa" - integrity sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ== + version "0.16.10" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.10.tgz#6f81b71ac37ff4ec7556861160f53bc5f058b185" + integrity sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA== dependencies: commander "^8.3.0" @@ -242,10 +308,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -markdown-it-front-matter@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.2.1.tgz#dca49a827bb3cebb0528452c1d87dff276eb28dc" - integrity sha512-ydUIqlKfDscRpRUTRcA3maeeUKn3Cl5EaKZSA+I/f0KOGCBurW7e+bbz59sxqkC3FA9Q2S2+t4mpkH9T0BCM6A== +markdown-it-front-matter@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.2.4.tgz#cf29bc8222149b53575699357b1ece697bf39507" + integrity sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w== markdown-it@^12.3.2: version "12.3.2" @@ -275,6 +341,21 @@ morphdom@^2.6.1: resolved "https://registry.yarnpkg.com/morphdom/-/morphdom-2.6.1.tgz#e868e24f989fa3183004b159aed643e628b4306e" integrity sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA== +node-html-parser@^6.1.5: + version "6.1.13" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4" + integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg== + dependencies: + css-select "^5.1.0" + he "1.2.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -297,6 +378,16 @@ vscode-jsonrpc@8.0.2: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz#f239ed2cd6004021b6550af9fd9d3e47eee3cac9" integrity sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ== +vscode-jsonrpc@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94" + integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw== + +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + vscode-languageclient@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz#f1f23ce8c8484aa11e4b7dfb24437d3e59bb61c6" @@ -314,7 +405,23 @@ vscode-languageserver-protocol@3.17.2: vscode-jsonrpc "8.0.2" vscode-languageserver-types "3.17.2" -vscode-languageserver-textdocument@^1.0.11: +vscode-languageserver-protocol@3.17.3: + version "3.17.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz#6d0d54da093f0c0ee3060b81612cce0f11060d57" + integrity sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA== + dependencies: + vscode-jsonrpc "8.1.0" + vscode-languageserver-types "3.17.3" + +vscode-languageserver-protocol@^3.17.1: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@^1.0.11, vscode-languageserver-textdocument@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== @@ -329,6 +436,35 @@ vscode-languageserver-types@3.17.2, vscode-languageserver-types@^3.17.1, vscode- resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2" integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA== +vscode-languageserver-types@3.17.3: + version "3.17.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64" + integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA== + +vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.17.3: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz#5024253718915d84576ce6662dd46a791498d827" + integrity sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw== + dependencies: + vscode-languageserver-protocol "3.17.3" + +vscode-markdown-languageserver@^0.5.0-alpha.8: + version "0.5.0-alpha.8" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageserver/-/vscode-markdown-languageserver-0.5.0-alpha.8.tgz#87ced4b241636b6aeda7aacc41badced0c2cc992" + integrity sha512-Bp6YXHy4EMQ8JpsmXpQHa78byLvv83wVnLmhVfTaJsZTBwF8IOJ56NBUasepg9L6QVffUA9T2H/PfBqEOGpeOQ== + dependencies: + "@vscode/l10n" "^0.0.11" + vscode-languageserver "^8.1.0" + vscode-languageserver-textdocument "^1.0.8" + vscode-languageserver-types "^3.17.3" + vscode-markdown-languageservice "^0.5.0-alpha.7" + vscode-uri "^3.0.7" + vscode-markdown-languageservice@^0.3.0-alpha.3: version "0.3.0-alpha.3" resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.3.0-alpha.3.tgz#219a4880cfc0ea037b5a1833bc0b0039bfd1e2db" @@ -340,11 +476,28 @@ vscode-markdown-languageservice@^0.3.0-alpha.3: vscode-languageserver-types "^3.17.1" vscode-uri "^3.0.3" +vscode-markdown-languageservice@^0.5.0-alpha.7: + version "0.5.0-alpha.7" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.7.tgz#0cc0939ea803d2afcb7a6e99b55feec664ec1b37" + integrity sha512-Iq9S5YGHm3D/UG9Usm8a/O5tYCo9FwaMF7nJsDQCxKgVZu5OzwOj3ixDkhoM+c8GKXiwt23DxhhWRuvI4odkTg== + dependencies: + "@vscode/l10n" "^0.0.10" + node-html-parser "^6.1.5" + picomatch "^2.3.1" + vscode-languageserver-protocol "^3.17.1" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.7" + vscode-uri@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA== +vscode-uri@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" diff --git a/extensions/markdown-math/notebook/katex.ts b/extensions/markdown-math/notebook/katex.ts index 94aad4f3c3b..ccc43046b32 100644 --- a/extensions/markdown-math/notebook/katex.ts +++ b/extensions/markdown-math/notebook/katex.ts @@ -51,7 +51,8 @@ export async function activate(ctx: RendererContext) { return md.use(katex, { globalGroup: true, enableBareBlocks: true, - macros + enableFencedBlocks: true, + macros, }); }); } diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 44b442b3df4..9669efc2435 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -56,6 +56,16 @@ "meta.embedded.math.markdown": "latex", "punctuation.definition.math.end.markdown": "latex" } + }, + { + "scopeName": "markdown.math.codeblock", + "path": "./syntaxes/md-math-fence.tmLanguage.json", + "injectTo": [ + "text.html.markdown" + ], + "embeddedLanguages": { + "meta.embedded.math.markdown": "latex" + } } ], "notebookRenderer": [ @@ -99,7 +109,7 @@ "build-notebook": "node ./esbuild" }, "dependencies": { - "@vscode/markdown-it-katex": "^1.0.3" + "@vscode/markdown-it-katex": "^1.1.0" }, "devDependencies": { "@types/markdown-it": "^0.0.0", diff --git a/extensions/markdown-math/src/extension.ts b/extensions/markdown-math/src/extension.ts index 1c27036b2fc..6491b0c1459 100644 --- a/extensions/markdown-math/src/extension.ts +++ b/extensions/markdown-math/src/extension.ts @@ -30,7 +30,11 @@ export function activate(context: vscode.ExtensionContext) { if (isEnabled()) { const katex = require('@vscode/markdown-it-katex').default; const settingsMacros = getMacros(); - const options = { globalGroup: true, macros: { ...settingsMacros } }; + const options = { + enableFencedBlocks: true, + globalGroup: true, + macros: { ...settingsMacros } + }; md.core.ruler.push('reset-katex-macros', () => { options.macros = { ...settingsMacros }; }); @@ -39,4 +43,4 @@ export function activate(context: vscode.ExtensionContext) { return md; } }; -} +} \ No newline at end of file diff --git a/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json b/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json index 43fd1bda5db..543568bf83e 100644 --- a/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json +++ b/extensions/markdown-math/syntaxes/md-math-block.tmLanguage.json @@ -82,4 +82,4 @@ } }, "scopeName": "markdown.math.block" -} +} \ No newline at end of file diff --git a/extensions/markdown-math/syntaxes/md-math-fence.tmLanguage.json b/extensions/markdown-math/syntaxes/md-math-fence.tmLanguage.json new file mode 100644 index 00000000000..556c579d3e7 --- /dev/null +++ b/extensions/markdown-math/syntaxes/md-math-fence.tmLanguage.json @@ -0,0 +1,28 @@ +{ + "fileTypes": [], + "injectionSelector": "L:markup.fenced_code.block.markdown", + "patterns": [ + { + "include": "#math-code-block" + } + ], + "repository": { + "math-code-block": { + "begin": "(?<=[`~])math(\\s+[^`~]*)?$", + "end": "(^|\\G)(?=\\s*[`~]{3,}\\s*$)", + "patterns": [ + { + "begin": "(^|\\G)(\\s*)(.*)", + "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", + "contentName": "meta.embedded.math.markdown", + "patterns": [ + { + "include": "text.html.markdown.math#math" + } + ] + } + ] + } + }, + "scopeName": "markdown.math.codeblock" +} diff --git a/extensions/markdown-math/yarn.lock b/extensions/markdown-math/yarn.lock index 38e50260d03..52f3ff9545e 100644 --- a/extensions/markdown-math/yarn.lock +++ b/extensions/markdown-math/yarn.lock @@ -12,10 +12,10 @@ resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.72.0.tgz#8943dc3cef0ced2dfb1e04c0a933bd289e7d5199" integrity sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw== -"@vscode/markdown-it-katex@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@vscode/markdown-it-katex/-/markdown-it-katex-1.0.3.tgz#5364e4dbcb0f7e7fd2fdab3847ba5d6b0c3ce9d9" - integrity sha512-a8ppdac0CG2lAQC6E6lT8dxmXkUk9gRtYNtILx31FyrPEwj875AAHc6tpRGeJBpWMpiMtcvz7ymWYBwYgxuFmw== +"@vscode/markdown-it-katex@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.0.tgz#e991b58f6eb7cf56aef74b98e1a5edc1494649bb" + integrity sha512-9cF2eJpsJOEs2V1cCAoJW/boKz9GQQLvZhNvI030K90z6ZE9lRGc9hDVvKut8zdFO2ObjwylPXXXVYvTdP2O2Q== dependencies: katex "^0.16.4" @@ -25,8 +25,8 @@ commander@^8.3.0: integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== katex@^0.16.4: - version "0.16.9" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.9.tgz#bc62d8f7abfea6e181250f85a56e4ef292dcb1fa" - integrity sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ== + version "0.16.10" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.10.tgz#6f81b71ac37ff4ec7556861160f53bc5f058b185" + integrity sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA== dependencies: commander "^8.3.0" diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index f55fb26c44e..cdda46fab32 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -169,7 +169,7 @@ "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index f93736b6b27..31f7cee0830 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -108,3 +110,8 @@ "@microsoft/1ds-core-js" "^4.0.3" "@microsoft/1ds-post-js" "^4.0.3" "@microsoft/applicationinsights-web-basic" "^3.0.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/microsoft-authentication/extension-browser.webpack.config.js b/extensions/microsoft-authentication/extension-browser.webpack.config.js index 2513c7d0f9c..c0f4b92069f 100644 --- a/extensions/microsoft-authentication/extension-browser.webpack.config.js +++ b/extensions/microsoft-authentication/extension-browser.webpack.config.js @@ -22,10 +22,8 @@ module.exports = withBrowserDefaults({ }, resolve: { alias: { - './node/crypto': path.resolve(__dirname, 'src/browser/crypto'), './node/authServer': path.resolve(__dirname, 'src/browser/authServer'), - './node/buffer': path.resolve(__dirname, 'src/browser/buffer'), - './node/fetch': path.resolve(__dirname, 'src/browser/fetch'), + './node/buffer': path.resolve(__dirname, 'src/browser/buffer') } } }); diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index c82eea19318..31a7d4bd7e8 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -109,14 +109,13 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", "@types/uuid": "8.0.0" }, "dependencies": { - "node-fetch": "2.6.7", "@azure/ms-rest-azure-env": "^2.0.0", "@vscode/extension-telemetry": "^0.9.0" }, diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 0417defe534..713f5f12e9a 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -6,12 +6,11 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { isSupportedEnvironment } from './common/uri'; -import { IntervalTimer, SequencerByKey } from './common/async'; +import { IntervalTimer, raceCancellationAndTimeoutError, SequencerByKey } from './common/async'; import { generateCodeChallenge, generateCodeVerifier, randomUUID } from './cryptoUtils'; import { BetterTokenStorage, IDidChangeInOtherWindowEvent } from './betterSecretStorage'; import { LoopbackAuthServer } from './node/authServer'; import { base64Decode } from './node/buffer'; -import { fetching } from './node/fetch'; import { UriEventHandler } from './UriEventHandler'; import TelemetryReporter from '@vscode/extension-telemetry'; import { Environment } from '@azure/ms-rest-azure-env'; @@ -203,11 +202,13 @@ export class AzureActiveDirectoryService { return this._sessionChangeEmitter.event; } - public getSessions(scopes?: string[]): Promise { + public getSessions(scopes?: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { if (!scopes) { this._logger.info('Getting sessions for all scopes...'); - const sessions = this._tokens.map(token => this.convertToSessionSync(token)); - this._logger.info(`Got ${sessions.length} sessions for all scopes...`); + const sessions = this._tokens + .filter(token => !account?.label || token.account.label === account.label) + .map(token => this.convertToSessionSync(token)); + this._logger.info(`Got ${sessions.length} sessions for all scopes${account ? ` for account '${account.label}'` : ''}...`); return Promise.resolve(sessions); } @@ -238,23 +239,43 @@ export class AzureActiveDirectoryService { tenant: this.getTenantId(scopes), }; - this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions`); - return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData)); + this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions` + account ? ` for ${account?.label}` : ''); + return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData, account)); } - private async doGetSessions(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Getting sessions`); + private async doGetSessions(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Getting sessions` + account ? ` for ${account?.label}` : ''); - const matchingTokens = this._tokens.filter(token => token.scope === scopeData.scopeStr); + const matchingTokens = this._tokens + .filter(token => token.scope === scopeData.scopeStr) + .filter(token => !account?.label || token.account.label === account.label); // If we still don't have a matching token try to get a new token from an existing token by using // the refreshToken. This is documented here: // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token // "Refresh tokens are valid for all permissions that your client has already received consent for." if (!matchingTokens.length) { - // Get a token with the correct client id. - const token = scopeData.clientId === DEFAULT_CLIENT_ID - ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) - : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)); + // Get a token with the correct client id and account. + let token: IToken | undefined; + for (const t of this._tokens) { + // No refresh token, so we can't make a new token from this session + if (!t.refreshToken) { + continue; + } + // Need to make sure the account matches if we were provided one + if (account?.label && t.account.label !== account.label) { + continue; + } + // If the client id is the default client id, then check for the absence of the VSCODE_CLIENT_ID scope + if (scopeData.clientId === DEFAULT_CLIENT_ID && !t.scope.includes('VSCODE_CLIENT_ID')) { + token = t; + break; + } + // If the client id is not the default client id, then check for the matching VSCODE_CLIENT_ID scope + if (scopeData.clientId !== DEFAULT_CLIENT_ID && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)) { + token = t; + break; + } + } if (token) { this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Found a matching token with a different scopes '${token.scope}'. Attempting to get a new session using the existing session.`); @@ -275,7 +296,7 @@ export class AzureActiveDirectoryService { .map(result => (result as PromiseFulfilledResult).value); } - public createSession(scopes: string[]): Promise { + public createSession(scopes: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); @@ -301,11 +322,11 @@ export class AzureActiveDirectoryService { }; this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`); - return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData)); + return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData, account)); } - private async doCreateSession(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Creating session`); + private async doCreateSession(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Creating session` + account ? ` for ${account?.label}` : ''); const runsRemote = vscode.env.remoteName !== undefined; const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; @@ -314,25 +335,27 @@ export class AzureActiveDirectoryService { throw new Error('Sign in to non-public clouds is not supported on the web.'); } - if (runsRemote || runsServerless) { - return this.createSessionWithoutLocalServer(scopeData); - } - - try { - return await this.createSessionWithLocalServer(scopeData); - } catch (e) { - this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`); - - // If the error was about starting the server, try directly hitting the login endpoint instead - if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { - return this.createSessionWithoutLocalServer(scopeData); + return await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Signing in to your account...'), cancellable: true }, async (_progress, token) => { + if (runsRemote || runsServerless) { + return await this.createSessionWithoutLocalServer(scopeData, account?.label, token); } - throw e; - } + try { + return await this.createSessionWithLocalServer(scopeData, account?.label, token); + } catch (e) { + this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`); + + // If the error was about starting the server, try directly hitting the login endpoint instead + if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { + return this.createSessionWithoutLocalServer(scopeData, account?.label, token); + } + + throw e; + } + }); } - private async createSessionWithLocalServer(scopeData: IScopeData) { + private async createSessionWithLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with local server`); const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); @@ -342,18 +365,22 @@ export class AzureActiveDirectoryService { client_id: scopeData.clientId, redirect_uri: redirectUrl, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); - const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`, this._env.activeDirectoryEndpointUrl).toString(); + }); + if (loginHint) { + qs.set('login_hint', loginHint); + } else { + qs.set('prompt', 'select_account'); + } + const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs.toString()}`, this._env.activeDirectoryEndpointUrl).toString(); const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); await server.start(); let codeToExchange; try { vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${server.port}/signin?nonce=${encodeURIComponent(server.nonce)}`)); - const { code } = await server.waitForOAuthResponse(); + const { code } = await raceCancellationAndTimeoutError(server.waitForOAuthResponse(), token, 1000 * 60 * 5); // 5 minutes codeToExchange = code; } finally { setTimeout(() => { @@ -368,7 +395,7 @@ export class AzureActiveDirectoryService { return session; } - private async createSessionWithoutLocalServer(scopeData: IScopeData): Promise { + private async createSessionWithoutLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow without local server`); let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = generateCodeVerifier(); @@ -381,28 +408,25 @@ export class AzureActiveDirectoryService { const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl); - signInUrl.search = new URLSearchParams({ + const qs = new URLSearchParams({ response_type: 'code', client_id: encodeURIComponent(scopeData.clientId), response_mode: 'query', redirect_uri: redirectUrl, state, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); + }); + if (loginHint) { + qs.append('login_hint', loginHint); + } else { + qs.append('prompt', 'select_account'); + } + signInUrl.search = qs.toString(); const uri = vscode.Uri.parse(signInUrl.toString()); vscode.env.openExternal(uri); - let inputBox: vscode.InputBox | undefined; - const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession) => void, reject) => { - const wait = setTimeout(() => { - clearTimeout(wait); - inputBox?.dispose(); - reject('Login timed out.'); - }, 1000 * 60 * 5); - }); const existingNonces = this._pendingNonces.get(scopeData.scopeStr) || []; this._pendingNonces.set(scopeData.scopeStr, [...existingNonces, nonce]); @@ -410,6 +434,7 @@ export class AzureActiveDirectoryService { // Register a single listener for the URI callback, in case the user starts the login process multiple times // before completing it. let existingPromise = this._codeExchangePromises.get(scopeData.scopeStr); + let inputBox: vscode.InputBox | undefined; if (!existingPromise) { if (isSupportedEnvironment(callbackUri)) { existingPromise = this.handleCodeResponse(scopeData); @@ -422,11 +447,12 @@ export class AzureActiveDirectoryService { this._codeVerfifiers.set(nonce, codeVerifier); - return Promise.race([existingPromise, timeoutPromise]) + return await raceCancellationAndTimeoutError(existingPromise, token, 1000 * 60 * 5) // 5 minutes .finally(() => { this._pendingNonces.delete(scopeData.scopeStr); this._codeExchangePromises.delete(scopeData.scopeStr); this._codeVerfifiers.delete(nonce); + inputBox?.dispose(); }); } @@ -779,7 +805,7 @@ export class AzureActiveDirectoryService { let result; let errorMessage: string | undefined; try { - result = await fetching(endpoint, { + result = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/extensions/microsoft-authentication/src/common/async.ts b/extensions/microsoft-authentication/src/common/async.ts index 527b5bbb399..641faaff0dd 100644 --- a/extensions/microsoft-authentication/src/common/async.ts +++ b/extensions/microsoft-authentication/src/common/async.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vscode'; +import { CancellationError, CancellationToken, Disposable } from 'vscode'; export class SequencerByKey { @@ -47,3 +47,36 @@ export class IntervalTimer extends Disposable { }, interval); } } + +/** + * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled. + * @see {@link raceCancellation} + */ +export function raceCancellationError(promise: Promise, token: CancellationToken): Promise { + return new Promise((resolve, reject) => { + const ref = token.onCancellationRequested(() => { + ref.dispose(); + reject(new CancellationError()); + }); + promise.then(resolve, reject).finally(() => ref.dispose()); + }); +} + +export class TimeoutError extends Error { + constructor() { + super('Timed out'); + } +} + +export function raceTimeoutError(promise: Promise, timeout: number): Promise { + return new Promise((resolve, reject) => { + const ref = setTimeout(() => { + reject(new CancellationError()); + }, timeout); + promise.then(resolve, reject).finally(() => clearTimeout(ref)); + }); +} + +export function raceCancellationAndTimeoutError(promise: Promise, token: CancellationToken, timeout: number): Promise { + return raceCancellationError(raceTimeoutError(promise, timeout), token); +} diff --git a/extensions/microsoft-authentication/src/cryptoUtils.ts b/extensions/microsoft-authentication/src/cryptoUtils.ts index 582dae74c3c..e608a81fc99 100644 --- a/extensions/microsoft-authentication/src/cryptoUtils.ts +++ b/extensions/microsoft-authentication/src/cryptoUtils.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { base64Encode } from './node/buffer'; -import { crypto } from './node/crypto'; export function randomUUID() { return crypto.randomUUID(); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 02cfb4643f4..87dc94e4c25 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -123,8 +123,8 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { onDidChangeSessions: loginService.onDidChangeSessions, - getSessions: (scopes: string[]) => loginService.getSessions(scopes), - createSession: async (scopes: string[]) => { + getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account), + createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { try { /* __GDPR__ "login" : { @@ -138,7 +138,7 @@ export async function activate(context: vscode.ExtensionContext) { scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), }); - return await loginService.createSession(scopes); + return await loginService.createSession(scopes, options?.account); } catch (e) { /* __GDPR__ "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } diff --git a/extensions/microsoft-authentication/src/node/authServer.ts b/extensions/microsoft-authentication/src/node/authServer.ts index de08c6fca0f..2d6a8d03861 100644 --- a/extensions/microsoft-authentication/src/node/authServer.ts +++ b/extensions/microsoft-authentication/src/node/authServer.ts @@ -110,20 +110,29 @@ export class LoopbackAuthServer implements ILoopbackServer { const code = reqUrl.searchParams.get('code') ?? undefined; const state = reqUrl.searchParams.get('state') ?? undefined; const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+'); + const error = reqUrl.searchParams.get('error') ?? undefined; + if (error) { + res.writeHead(302, { location: `/?error=${reqUrl.searchParams.get('error_description')}` }); + res.end(); + deferred.reject(new Error(error)); + break; + } if (!code || !state || !nonce) { res.writeHead(400); res.end(); - return; + break; } if (this.state !== state) { res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` }); res.end(); - throw new Error('State does not match.'); + deferred.reject(new Error('State does not match.')); + break; } if (this.nonce !== nonce) { res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); res.end(); - throw new Error('Nonce does not match.'); + deferred.reject(new Error('Nonce does not match.')); + break; } deferred.resolve({ code, state }); res.writeHead(302, { location: '/' }); diff --git a/extensions/microsoft-authentication/src/node/fetch.ts b/extensions/microsoft-authentication/src/node/fetch.ts deleted file mode 100644 index 58718078e69..00000000000 --- a/extensions/microsoft-authentication/src/node/fetch.ts +++ /dev/null @@ -1,7 +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 fetch from 'node-fetch'; - -export const fetching = fetch; diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index afa82e5759f..703d54dd626 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -113,10 +113,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/randombytes@^2.0.0": version "2.0.0" @@ -184,27 +186,7 @@ mime-types@^2.1.12: dependencies: mime-db "1.44.0" -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index ef9240df2b7..8f5fa908cb9 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -11,7 +11,7 @@ import { formatStackTrace } from './stackTraceHelper'; function clearContainer(container: HTMLElement) { while (container.firstChild) { - container.removeChild(container.firstChild); + container.firstChild.remove(); } } @@ -184,28 +184,35 @@ function renderError( return disposableStore; } + const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message; + if (err.stack) { + const minimalError = ctx.settings.minimalError && !!headerMessage?.length; outputElement.classList.add('traceback'); - const stackTrace = formatStackTrace(err.stack); + const { formattedStack, errorLocation } = formatStackTrace(err.stack); - const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); - const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml, linkifyFilePaths: ctx.settings.linkifyFilePaths }; + const outputScrolling = !minimalError && scrollingEnabled(outputInfo, ctx.settings); + const lineLimit = minimalError ? 1000 : ctx.settings.lineLimit; + const outputOptions = { linesLimit: lineLimit, scrollable: outputScrolling, trustHtml, linkifyFilePaths: false }; - const content = createOutputContent(outputInfo.id, stackTrace ?? '', outputOptions); - const contentParent = document.createElement('div'); - contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap); + const content = createOutputContent(outputInfo.id, formattedStack, outputOptions); + const stackTraceElement = document.createElement('div'); + stackTraceElement.appendChild(content); + outputElement.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { - contentParent.classList.toggle('word-wrap', e.outputWordWrap); + outputElement.classList.toggle('word-wrap', e.outputWordWrap); })); - contentParent.classList.toggle('scrollable', outputScrolling); - contentParent.appendChild(content); - outputElement.appendChild(contentParent); - initializeScroll(contentParent, disposableStore); + if (minimalError) { + createMinimalError(errorLocation, headerMessage, stackTraceElement, outputElement); + } else { + stackTraceElement.classList.toggle('scrollable', outputScrolling); + outputElement.appendChild(stackTraceElement); + initializeScroll(stackTraceElement, disposableStore); + } } else { const header = document.createElement('div'); - const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message; if (headerMessage) { header.innerText = headerMessage; outputElement.appendChild(header); @@ -216,6 +223,54 @@ function renderError( return disposableStore; } +function createMinimalError(errorLocation: string | undefined, headerMessage: string, stackTrace: HTMLDivElement, outputElement: HTMLElement) { + const outputDiv = document.createElement('div'); + const headerSection = document.createElement('div'); + headerSection.classList.add('error-output-header'); + + if (errorLocation && errorLocation.indexOf(' { + e.preventDefault(); + const hidden = stackTrace.style.display === 'none'; + stackTrace.style.display = hidden ? '' : 'none'; + toggleStackLink.innerText = hidden ? 'Hide Details' : 'Show Details'; + }; + + outputDiv.appendChild(stackTrace); + stackTrace.style.display = 'none'; + outputElement.appendChild(outputDiv); +} + function getPreviousMatchingContentGroup(outputElement: HTMLElement) { const outputContainer = outputElement.parentElement; let match: HTMLElement | undefined = undefined; @@ -323,15 +378,15 @@ function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, contentParent = document.createElement('div'); contentParent.appendChild(newContent); while (outputElement.firstChild) { - outputElement.removeChild(outputElement.firstChild); + outputElement.firstChild.remove(); } outputElement.appendChild(contentParent); } contentParent.classList.toggle('scrollable', outputScrolling); - contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap); + outputElement.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { - contentParent!.classList.toggle('word-wrap', e.outputWordWrap); + outputElement.classList.toggle('word-wrap', e.outputWordWrap); })); initializeScroll(contentParent, disposableStore, scrollTop); @@ -349,9 +404,10 @@ function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRi const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, linkifyFilePaths: ctx.settings.linkifyFilePaths }; const content = createOutputContent(outputInfo.id, text, outputOptions); content.classList.add('output-plaintext'); - if (ctx.settings.outputWordWrap) { - content.classList.add('word-wrap'); - } + outputElement.classList.toggle('word-wrap', ctx.settings.outputWordWrap); + disposableStore.push(ctx.onDidChangeSettings(e => { + outputElement.classList.toggle('word-wrap', e.outputWordWrap); + })); content.classList.toggle('scrollable', outputScrolling); outputElement.appendChild(content); @@ -390,7 +446,7 @@ export const activate: ActivationFunction = (ctx) => { white-space: pre; } /* When wordwrap turned on, force it to pre-wrap */ - #container div.output_container .word-wrap span { + #container div.output_container .word-wrap { white-space: pre-wrap; } #container div.output>div { @@ -406,7 +462,7 @@ export const activate: ActivationFunction = (ctx) => { border-color: var(--theme-input-focus-border-color); } #container div.output .scrollable { - overflow-y: scroll; + overflow-y: auto; max-height: var(--notebook-cell-output-max-height); } #container div.output .scrollable.scrollbar-visible { @@ -449,6 +505,35 @@ export const activate: ActivationFunction = (ctx) => { .traceback .code-underline { text-decoration: underline; } + #container ul.error-output-actions { + margin: 0px; + padding: 6px 0px 0px 6px; + padding-inline-start: 0px; + } + #container .error-output-actions li { + padding: 0px 4px 0px 4px; + border-radius: 5px; + height: 20px; + display: inline-flex; + cursor: pointer; + border: solid 1px var(--vscode-notebook-cellToolbarSeparator); + } + #container .error-output-actions li.hover { + background-color: var(--vscode-toolbar-hoverBackground); + } + #container .error-output-actions li:focus-within { + border-color: var(--theme-input-focus-border-color); + } + #container .error-output-actions a:focus { + outline: 0; + } + #container .error-output-actions li a { + color: var(--vscode-foreground); + text-decoration: none; + } + #container .error-output-header a { + padding-right: 12px; + } `; document.body.appendChild(style); diff --git a/extensions/notebook-renderers/src/rendererTypes.ts b/extensions/notebook-renderers/src/rendererTypes.ts index e1fc869a301..bab7a34af9f 100644 --- a/extensions/notebook-renderers/src/rendererTypes.ts +++ b/extensions/notebook-renderers/src/rendererTypes.ts @@ -33,6 +33,7 @@ export interface RenderOptions { readonly outputScrolling: boolean; readonly outputWordWrap: boolean; readonly linkifyFilePaths: boolean; + readonly minimalError: boolean; } export type IRichRenderContext = RendererContext & { readonly settings: RenderOptions; readonly onDidChangeSettings: Event }; diff --git a/extensions/notebook-renderers/src/stackTraceHelper.ts b/extensions/notebook-renderers/src/stackTraceHelper.ts index 943877ad59d..ecf0eddb40e 100644 --- a/extensions/notebook-renderers/src/stackTraceHelper.ts +++ b/extensions/notebook-renderers/src/stackTraceHelper.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export function formatStackTrace(stack: string) { +export function formatStackTrace(stack: string): { formattedStack: string; errorLocation?: string } { let cleaned: string; // Ansi colors are described here: // https://en.wikipedia.org/wiki/ANSI_escape_code under the SGR section @@ -26,7 +26,7 @@ export function formatStackTrace(stack: string) { return linkifyStack(cleaned); } - return cleaned; + return { formattedStack: cleaned }; } const formatSequence = /\u001b\[.+?m/g; @@ -49,10 +49,11 @@ type fileLocation = { kind: 'file'; path: string }; type location = cellLocation | fileLocation; -function linkifyStack(stack: string) { +function linkifyStack(stack: string): { formattedStack: string; errorLocation?: string } { const lines = stack.split('\n'); let fileOrCell: location | undefined; + let locationLink = ''; for (const i in lines) { @@ -67,7 +68,9 @@ function linkifyStack(stack: string) { kind: 'cell', path: stripFormatting(original.replace(cellRegex, 'vscode-notebook-cell:?execution_count=$')) }; - lines[i] = original.replace(cellRegex, `$\'>line $`); + const link = original.replace(cellRegex, `\'>line $`); + lines[i] = original.replace(cellRegex, `$${link}`); + locationLink = locationLink || link; continue; } else if (inputRegex.test(original)) { @@ -75,7 +78,8 @@ function linkifyStack(stack: string) { kind: 'cell', path: stripFormatting(original.replace(inputRegex, 'vscode-notebook-cell:?execution_count=$')) }; - lines[i] = original.replace(inputRegex, `Input \'>$$`); + const link = original.replace(inputRegex, `$`); + lines[i] = original.replace(inputRegex, `Input ${link}$`); continue; } else if (!fileOrCell || original.trim() === '') { @@ -94,5 +98,6 @@ function linkifyStack(stack: string) { } } - return lines.join('\n'); + const errorLocation = locationLink; + return { formattedStack: lines.join('\n'), errorLocation }; } diff --git a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts index 07ec9cdf9ea..dfc7e2b15f8 100644 --- a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts +++ b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts @@ -152,7 +152,7 @@ suite('Notebook builtin output renderer', () => { const inserted = outputElement.firstChild as HTMLElement; assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); assert.ok(outputElement.classList.contains('remove-padding'), `Padding should be removed for scrollable outputs ${outputElement.classList}`); - assert.ok(inserted.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), + assert.ok(outputElement.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), `output content classList should contain word-wrap and scrollable ${inserted.classList}`); assert.ok(inserted.innerHTML.indexOf('>content -1, `Content was not added to output element: ${outputElement.innerHTML}`); }); @@ -169,7 +169,7 @@ suite('Notebook builtin output renderer', () => { const inserted = outputElement.firstChild as HTMLElement; assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); assert.ok(outputElement.classList.contains('remove-padding'), `Padding should be removed for non-scrollable outputs: ${outputElement.classList}`); - assert.ok(!inserted.classList.contains('word-wrap') && !inserted.classList.contains('scrollable'), + assert.ok(!outputElement.classList.contains('word-wrap') && !inserted.classList.contains('scrollable'), `output content classList should not contain word-wrap and scrollable ${inserted.classList}`); assert.ok(inserted.innerHTML.indexOf('>content -1, `Content was not added to output element: ${outputElement.innerHTML}`); }); @@ -316,7 +316,7 @@ suite('Notebook builtin output renderer', () => { const inserted = outputElement.firstChild as HTMLElement; assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); assert.ok(outputElement.classList.contains('remove-padding'), 'Padding should be removed for scrollable outputs'); - assert.ok(inserted.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), + assert.ok(outputElement.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), `output content classList should contain word-wrap and scrollable ${inserted.classList}`); assert.ok(inserted.innerHTML.indexOf('>Expected type `str`, but received type') > -1, `Content was not added to output element:\n ${outputElement.innerHTML}`); assert.ok(inserted.textContent!.indexOf('Expected type `str`, but received type ``') > -1, `Content was not added to output element:\n ${outputElement.textContent}`); @@ -465,7 +465,7 @@ suite('Notebook builtin output renderer', () => { fireSettingsChange({ outputWordWrap: true, outputScrolling: true }); const inserted = outputElement.firstChild as HTMLElement; - assert.ok(inserted.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), + assert.ok(outputElement.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), `output content classList should contain word-wrap and scrollable ${inserted.classList}`); }); diff --git a/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts b/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts index c4e99179086..54ec15b428c 100644 --- a/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts +++ b/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts @@ -16,7 +16,7 @@ suite('StackTraceHelper', () => { '@Main c:\\src\\test\\3\\otherlanguages\\julia.ipynb: 3\n' + '[2] top - level scope\n' + '@c:\\src\\test\\3\\otherlanguages\\julia.ipynb: 1; '; - assert.equal(formatStackTrace(stack), stack); + assert.equal(formatStackTrace(stack).formattedStack, stack); }); const formatSequence = /\u001b\[.+?m/g; @@ -37,10 +37,12 @@ suite('StackTraceHelper', () => { '\u001b[1;32m----> 2\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m\n\n' + '\u001b[1;31mException\u001b[0m\n:'; - const formatted = stripAsciiFormatting(formatStackTrace(stack)); - assert.ok(formatted.indexOf('Cell In[3], line 2') > 0, 'Missing line link in ' + formatted); - assert.ok(formatted.indexOf('2') > 0, 'Missing frame link in ' + formatted); - assert.ok(formatted.indexOf('2') > 0, 'Missing frame link in ' + formatted); + const { formattedStack, errorLocation } = formatStackTrace(stack); + const cleanStack = stripAsciiFormatting(formattedStack); + assert.ok(cleanStack.indexOf('Cell In[3], line 2') > 0, 'Missing line link in ' + cleanStack); + assert.ok(cleanStack.indexOf('2') > 0, 'Missing frame link in ' + cleanStack); + assert.ok(cleanStack.indexOf('2') > 0, 'Missing frame link in ' + cleanStack); + assert.equal(errorLocation, 'line 2'); }); test('IPython stack line numbers are linkified for IPython 8.3', () => { @@ -65,9 +67,10 @@ suite('StackTraceHelper', () => { '\n' + '\u001b[1;31mException\u001b[0m:\n'; - const formatted = stripAsciiFormatting(formatStackTrace(stack)); - assert.ok(formatted.indexOf('Input \'>In [2], in ') > 0, 'Missing cell link in ' + formatted); - assert.ok(formatted.indexOf('Input \'>In [1], in myfunc()') > 0, 'Missing cell link in ' + formatted); + const { formattedStack } = formatStackTrace(stack); + const formatted = stripAsciiFormatting(formattedStack); + assert.ok(formatted.indexOf('Input In [2], in ') > 0, 'Missing cell link in ' + formatted); + assert.ok(formatted.indexOf('Input In [1], in myfunc()') > 0, 'Missing cell link in ' + formatted); assert.ok(formatted.indexOf('5') > 0, 'Missing frame link in ' + formatted); }); @@ -82,7 +85,7 @@ suite('StackTraceHelper', () => { '\u001b[1;32m----> 2\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m\n\n' + '\u001b[1;31mException\u001b[0m\n:'; - const formatted = formatStackTrace(stack); + const formatted = formatStackTrace(stack).formattedStack; assert.ok(!/\d<\/a>/.test(formatted), formatted); }); @@ -97,7 +100,7 @@ suite('StackTraceHelper', () => { 'a 1 print(\n' + ' 1a print(\n'; - const formattedLines = formatStackTrace(stack).split('\n'); + const formattedLines = formatStackTrace(stack).formattedStack.split('\n'); assert.ok(/ assert.ok(!//.test(line), 'line should not contain a link: ' + line)); }); diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts index b49dbb6ad8d..9c080c7f9e4 100644 --- a/extensions/notebook-renderers/src/textHelper.ts +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -71,6 +71,11 @@ function generateNestedViewAllElement(outputId: string) { function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, linkOptions: LinkOptions) { const container = document.createElement('div'); + container.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: 'text', + outputId: id, + 'preventDefaultContextMenuItems': true + })); const lineCount = buffer.length; if (lineCount <= linesLimit) { @@ -95,6 +100,11 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number function scrollableArrayOfString(id: string, buffer: string[], linkOptions: LinkOptions) { const element = document.createElement('div'); + element.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: 'text', + outputId: id, + 'preventDefaultContextMenuItems': true + })); if (buffer.length > softScrollableLineLimit) { element.appendChild(generateNestedViewAllElement(id)); } diff --git a/extensions/notebook-renderers/yarn.lock b/extensions/notebook-renderers/yarn.lock index 3cbe531e0fd..00c3e704dba 100644 --- a/extensions/notebook-renderers/yarn.lock +++ b/extensions/notebook-renderers/yarn.lock @@ -408,9 +408,9 @@ word-wrap@~1.2.3: integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^4.0.0: version "4.0.0" diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 37411d32e52..545ce102ab5 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@types/minimatch": "^5.1.2", - "@types/node": "18.x", + "@types/node": "20.x", "@types/which": "^3.0.0" }, "main": "./out/npmMain", diff --git a/extensions/npm/src/features/packageJSONContribution.ts b/extensions/npm/src/features/packageJSONContribution.ts index f2318371673..999f39664f1 100644 --- a/extensions/npm/src/features/packageJSONContribution.ts +++ b/extensions/npm/src/features/packageJSONContribution.ts @@ -287,7 +287,19 @@ export class PackageJSONContribution implements IJSONContribution { return new Promise((resolve, _reject) => { const args = ['view', '--json', '--', pack, 'description', 'dist-tags.latest', 'homepage', 'version', 'time']; const cwd = resource && resource.scheme === 'file' ? dirname(resource.fsPath) : undefined; - cp.execFile(npmCommandPath, args, { cwd }, (error, stdout) => { + + // corepack npm wrapper would automatically update package.json. disable that behavior. + // COREPACK_ENABLE_AUTO_PIN disables the package.json overwrite, and + // COREPACK_ENABLE_PROJECT_SPEC makes the npm view command succeed + // even if packageManager specified a package manager other than npm. + const env = { ...process.env, COREPACK_ENABLE_AUTO_PIN: '0', COREPACK_ENABLE_PROJECT_SPEC: '0' }; + let options: cp.ExecFileOptions = { cwd, env }; + let commandPath: string = npmCommandPath; + if (process.platform === 'win32') { + options = { cwd, env, shell: true }; + commandPath = `"${npmCommandPath}"`; + } + cp.execFile(commandPath, args, options, (error, stdout) => { if (!error) { try { const content = JSON.parse(stdout); diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 7dad0575479..be4b192c67d 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/which@^3.0.0": version "3.0.0" @@ -37,21 +39,21 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -181,6 +183,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-uri@^3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" diff --git a/extensions/package.json b/extensions/package.json index ab93f194b51..c918c19c535 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,14 +4,17 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "5.4" + "typescript": "^5.5.4" }, "scripts": { "postinstall": "node ./postinstall.mjs" }, "devDependencies": { "@parcel/watcher": "2.1.0", - "esbuild": "0.20.0", + "esbuild": "0.23.0", "vscode-grammar-updater": "^1.1.0" + }, + "resolutions": { + "node-gyp-build": "4.8.1" } } diff --git a/extensions/php-language-features/package.json b/extensions/php-language-features/package.json index 8963fc796f4..989213b6c0c 100644 --- a/extensions/php-language-features/package.json +++ b/extensions/php-language-features/package.json @@ -77,7 +77,7 @@ "which": "^2.0.2" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/which": "^2.0.0" }, "repository": { diff --git a/extensions/php-language-features/yarn.lock b/extensions/php-language-features/yarn.lock index 4c2e01e4b71..ea9947b69e3 100644 --- a/extensions/php-language-features/yarn.lock +++ b/extensions/php-language-features/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/which@^2.0.0": version "2.0.0" @@ -17,6 +19,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/extensions/powershell/package.json b/extensions/powershell/package.json index 0fd7abd4149..d73e5e72fad 100644 --- a/extensions/powershell/package.json +++ b/extensions/powershell/package.json @@ -24,7 +24,8 @@ "PowerShell", "powershell", "ps", - "ps1" + "ps1", + "pwsh" ], "firstLine": "^#!\\s*/.*\\bpwsh\\b", "configuration": "./language-configuration.json" diff --git a/extensions/razor/cgmanifest.json b/extensions/razor/cgmanifest.json index d3685974bdb..b8b0e5dae4f 100644 --- a/extensions/razor/cgmanifest.json +++ b/extensions/razor/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/razor", "repositoryUrl": "https://github.com/dotnet/razor", - "commitHash": "f01e110af179981942987384d2b5d4e489eab014" + "commitHash": "39159764277f3c80a786d8872eba7730da3d7ef0" } }, "license": "MIT", diff --git a/extensions/razor/syntaxes/cshtml.tmLanguage.json b/extensions/razor/syntaxes/cshtml.tmLanguage.json index 389a6daf249..71055e66e10 100644 --- a/extensions/razor/syntaxes/cshtml.tmLanguage.json +++ b/extensions/razor/syntaxes/cshtml.tmLanguage.json @@ -4,9 +4,31 @@ "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/razor/commit/f01e110af179981942987384d2b5d4e489eab014", + "version": "https://github.com/dotnet/razor/commit/39159764277f3c80a786d8872eba7730da3d7ef0", "name": "ASP.NET Razor", "scopeName": "text.html.cshtml", + "injections": { + "string.quoted.double.html": { + "patterns": [ + { + "include": "#explicit-razor-expression" + }, + { + "include": "#implicit-expression" + } + ] + }, + "string.quoted.single.html": { + "patterns": [ + { + "include": "#explicit-razor-expression" + }, + { + "include": "#implicit-expression" + } + ] + } + }, "patterns": [ { "include": "#razor-control-structures" diff --git a/extensions/references-view/package.json b/extensions/references-view/package.json index 228332773c6..9566a965c76 100644 --- a/extensions/references-view/package.json +++ b/extensions/references-view/package.json @@ -399,6 +399,6 @@ "watch": "npx gulp watch-extension:references-view" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" } } diff --git a/extensions/references-view/yarn.lock b/extensions/references-view/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/references-view/yarn.lock +++ b/extensions/references-view/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/rust/cgmanifest.json b/extensions/rust/cgmanifest.json index d887c4863f1..4149933544e 100644 --- a/extensions/rust/cgmanifest.json +++ b/extensions/rust/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "rust-syntax", "repositoryUrl": "https://github.com/dustypomerleau/rust-syntax", - "commitHash": "ffa06ae9d52ea5464d3047cdbdc6bb970a665d23" + "commitHash": "cf3c686a50295380ce9994218138691f8767870c" } }, "license": "MIT", diff --git a/extensions/rust/syntaxes/rust.tmLanguage.json b/extensions/rust/syntaxes/rust.tmLanguage.json index 075400eb958..dcf4c44f8fd 100644 --- a/extensions/rust/syntaxes/rust.tmLanguage.json +++ b/extensions/rust/syntaxes/rust.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/dustypomerleau/rust-syntax/commit/ffa06ae9d52ea5464d3047cdbdc6bb970a665d23", + "version": "https://github.com/dustypomerleau/rust-syntax/commit/cf3c686a50295380ce9994218138691f8767870c", "name": "Rust", "scopeName": "source.rust", "patterns": [ @@ -709,7 +709,7 @@ { "comment": "other keywords", "name": "keyword.other.rust", - "match": "\\b(as|async|become|box|dyn|move|final|impl|in|override|priv|pub|ref|typeof|union|unsafe|unsized|use|virtual|where)\\b" + "match": "\\b(as|async|become|box|dyn|move|final|gen|impl|in|override|priv|pub|ref|typeof|union|unsafe|unsized|use|virtual|where)\\b" }, { "comment": "fn", diff --git a/extensions/scss/cgmanifest.json b/extensions/scss/cgmanifest.json index a67a4f54609..12247769ce2 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": "303bbf0c250fe380b9e57375598cfd916110758b" + "commitHash": "f52ab12f7f9346cc2568129d8c4419bd3d506b47" } }, "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.61.4" + "version": "0.62.1" } ], "version": 1 diff --git a/extensions/shellscript/cgmanifest.json b/extensions/shellscript/cgmanifest.json index d12320c1b95..73c65b96f2c 100644 --- a/extensions/shellscript/cgmanifest.json +++ b/extensions/shellscript/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jeff-hykin/better-shell-syntax", "repositoryUrl": "https://github.com/jeff-hykin/better-shell-syntax", - "commitHash": "4ba5d703087cac3c60cd57b206fd1cea0ff959cc" + "commitHash": "35020b0bd79a90d3b262b4c13a8bb0b33adc1f45" } }, "license": "MIT", - "version": "1.7.1" + "version": "1.8.7" } ], "version": 1 diff --git a/extensions/shellscript/package.json b/extensions/shellscript/package.json index 6f9e7072ab8..93333abd313 100644 --- a/extensions/shellscript/package.json +++ b/extensions/shellscript/package.json @@ -34,6 +34,7 @@ ".bash_profile", ".bash_login", ".ebuild", + ".eclass", ".profile", ".bash_logout", ".xprofile", diff --git a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json index 21766dc8477..255638d7db7 100644 --- a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json +++ b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/4ba5d703087cac3c60cd57b206fd1cea0ff959cc", + "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/35020b0bd79a90d3b262b4c13a8bb0b33adc1f45", "name": "Shell Script", "scopeName": "source.shell", "patterns": [ @@ -14,7 +14,7 @@ ], "repository": { "alias_statement": { - "begin": "(?:(alias)(?:[ \\t]*+)((?:(?:((?\\(\\)\\$`\\\\\"\\|]+)(?!>))", + "match": "(?:[ \\t]*+)((?:[^ \t\n>&;<>\\(\\)\\$`\\\\\"'<\\|]+)(?!>))", "captures": { "1": { "name": "string.unquoted.argument.shell", @@ -137,53 +123,117 @@ } }, { - "include": "#normal_statement_context" + "include": "#normal_context" } ] }, - "array_value": { - "begin": "(?:[ \\t]*+)(?:(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))(?:[ \\t]*+)((?:(?:((?|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/)))(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))|((?!\"|'|\\\\\\n?$)(?:[^!'\" \\t\\n\\r]+?)))(?:(?= |\\t)|(?:(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?|#|\\n|$|;|[ \\t]))(?!nocorrect |nocorrect\t|nocorrect$|readonly |readonly\t|readonly$|function |function\t|function$|foreach |foreach\t|foreach$|coproc |coproc\t|coproc$|logout |logout\t|logout$|export |export\t|export$|select |select\t|select$|repeat |repeat\t|repeat$|pushd |pushd\t|pushd$|until |until\t|until$|while |while\t|while$|local |local\t|local$|case |case\t|case$|done |done\t|done$|elif |elif\t|elif$|else |else\t|else$|esac |esac\t|esac$|popd |popd\t|popd$|then |then\t|then$|time |time\t|time$|for |for\t|for$|end |end\t|end$|fi |fi\t|fi$|do |do\t|do$|in |in\t|in$|if |if\t|if$))(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))|((?!\"|'|\\\\\\n?$)(?:[^!'\"<> \\t\\n\\r]+?)))(?:(?= |\\t)|(?:(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?]+))", + "captures": { + "1": { + "name": "entity.name.function.call.shell entity.name.command.shell" + } + } + }, + { + "begin": "(?:(?:\\G|(?|#|\\n|$|;|[ \\t]))(?!nocorrect |nocorrect\t|nocorrect$|readonly |readonly\t|readonly$|function |function\t|function$|foreach |foreach\t|foreach$|coproc |coproc\t|coproc$|logout |logout\t|logout$|export |export\t|export$|select |select\t|select$|repeat |repeat\t|repeat$|pushd |pushd\t|pushd$|until |until\t|until$|while |while\t|while$|local |local\t|local$|case |case\t|case$|done |done\t|done$|elif |elif\t|elif$|else |else\t|else$|esac |esac\t|esac$|popd |popd\t|popd$|then |then\t|then$|time |time\t|time$|for |for\t|for$|end |end\t|end$|fi |fi\t|fi$|do |do\t|do$|in |in\t|in$|if |if\t|if$)(?!\\\\\\n?$)))", + "end": "(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?|&&|\\|\\|", @@ -1245,6 +1282,9 @@ { "include": "#regex_comparison" }, + { + "include": "#arithmetic_no_dollar" + }, { "include": "#logical-expression" }, @@ -1291,7 +1331,7 @@ "include": "#pathname" }, { - "include": "#keyword" + "include": "#floating_keyword" }, { "include": "#support" @@ -1340,26 +1380,6 @@ }, "loop": { "patterns": [ - { - "begin": "(?<=^|;|&|\\s)(for)\\s+(?=\\({2})", - "beginCaptures": { - "1": { - "name": "keyword.control.shell" - } - }, - "end": "(?<=^|;|&|\\s)(?:done|(?=\\)))(?=\\s|;|&|$|\\))", - "endCaptures": { - "0": { - "name": "keyword.control.shell" - } - }, - "name": "meta.scope.for-loop.shell", - "patterns": [ - { - "include": "#initial_context" - } - ] - }, { "begin": "(?<=^|;|&|\\s)(for)\\s+(.+?)\\s+(in)(?=\\s|;|&|$)", "beginCaptures": { @@ -1438,13 +1458,13 @@ "begin": "(?<=^|;|&|\\s)if(?=\\s|;|&|$)", "beginCaptures": { "0": { - "name": "keyword.control.shell" + "name": "keyword.control.if.shell" } }, "end": "(?<=^|;|&|\\s)fi(?=\\s|;|&|$)", "endCaptures": { "0": { - "name": "keyword.control.shell" + "name": "keyword.control.fi.shell" } }, "name": "meta.scope.if-block.shell", @@ -1469,6 +1489,10 @@ "match": "0[xX][0-9A-Fa-f]+", "name": "constant.numeric.hex.shell" }, + { + "match": ";", + "name": "punctuation.separator.semicolon.range" + }, { "match": "0\\d+", "name": "constant.numeric.octal.shell" @@ -1487,6 +1511,30 @@ } ] }, + "math_operators": { + "patterns": [ + { + "match": "\\+{1,2}|-{1,2}|!|~|\\*{1,2}|/|%|<[<=]?|>[>=]?|==|!=|^|\\|{1,2}|&{1,2}|\\?|\\:|,|=|[*/%+\\-&^|]=|<<=|>>=", + "name": "keyword.operator.arithmetic.shell" + }, + { + "match": "0[xX][0-9A-Fa-f]+", + "name": "constant.numeric.hex.shell" + }, + { + "match": "0\\d+", + "name": "constant.numeric.octal.shell" + }, + { + "match": "\\d{1,2}#[0-9a-zA-Z@_]+", + "name": "constant.numeric.other.shell" + }, + { + "match": "\\d+", + "name": "constant.numeric.integer.shell" + } + ] + }, "misc_ranges": { "patterns": [ { @@ -1496,47 +1544,10 @@ "include": "#logical_expression_double" }, { - "begin": "\\(\\(", - "end": "\\)\\)", - "beginCaptures": { - "0": { - "name": "punctuation.section.arithmetic.shell" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.arithmetic.shell" - } - }, - "name": "meta.arithmetic.shell", - "patterns": [ - { - "include": "#math" - } - ] + "include": "#subshell_dollar" }, { - "begin": "(?|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/))(?!\\\\\\n?$)))", - "end": "(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?>?)(?:[ \\t]*+)([^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+))", + "match": "(?:(>>?)(?:[ \\t]*+)([^ \t\n>&;<>\\(\\)\\$`\\\\\"'<\\|]+))", "captures": { "1": { "name": "keyword.operator.redirect.shell" @@ -1952,69 +2031,15 @@ } }, "simple_unquoted": { - "match": "[^ \\t\\n'&;<>\\(\\)\\$`\\\\\"\\|]", + "match": "[^ \\t\\n>&;<>\\(\\)\\$`\\\\\"'<\\|]", "name": "string.unquoted.shell" }, + "special_expansion": { + "match": "!|:[-=?]?|\\*|@|##|#|%%|%|\\/", + "name": "keyword.operator.expansion.shell" + }, "start_of_command": { - "match": "(?:(?:[ \\t]*+)(?:(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?!foreach\\b(?!\\/)|select\\b(?!\\/)|repeat\\b(?!\\/)|until\\b(?!\\/)|while\\b(?!\\/)|case\\b(?!\\/)|done\\b(?!\\/)|elif\\b(?!\\/)|else\\b(?!\\/)|esac\\b(?!\\/)|then\\b(?!\\/)|for\\b(?!\\/)|end\\b(?!\\/)|in\\b(?!\\/)|fi\\b(?!\\/)|do\\b(?!\\/)|if\\b(?!\\/))(?!\\\\\\n?$)))" - }, - "start_of_double_quoted_command_name": { - "match": "(?:(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?:(?:(?:[ \\t]*+)((?:[^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+)(?!>)))?)(?:(?:\\$\")|\"))", - "captures": { - "1": { - "name": "entity.name.function.call.shell entity.name.command.shell", - "patterns": [ - { - "match": "\\*", - "name": "variable.language.special.wildcard.shell" - }, - { - "include": "#variable" - }, - { - "include": "#numeric_literal" - }, - { - "match": "(?|#|\\n|$|;|[ \\t]))(?:(?:(?:[ \\t]*+)((?:[^ \t\n'&;<>\\(\\)\\$`\\\\\"\\|]+)(?!>)))?)(?:(?:\\$')|'))", - "captures": { - "1": { - "name": "entity.name.function.call.shell entity.name.command.shell", - "patterns": [ - { - "match": "\\*", - "name": "variable.language.special.wildcard.shell" - }, - { - "include": "#variable" - }, - { - "include": "#numeric_literal" - }, - { - "match": "(?|#|\\n|$|;|[ \\t]))(?!nocorrect |nocorrect\t|nocorrect$|readonly |readonly\t|readonly$|function |function\t|function$|foreach |foreach\t|foreach$|coproc |coproc\t|coproc$|logout |logout\t|logout$|export |export\t|export$|select |select\t|select$|repeat |repeat\t|repeat$|pushd |pushd\t|pushd$|until |until\t|until$|while |while\t|while$|local |local\t|local$|case |case\t|case$|done |done\t|done$|elif |elif\t|elif$|else |else\t|else$|esac |esac\t|esac$|popd |popd\t|popd$|then |then\t|then$|time |time\t|time$|for |for\t|for$|end |end\t|end$|fi |fi\t|fi$|do |do\t|do$|in |in\t|in$|if |if\t|if$)(?!\\\\\\n?$)))" }, "string": { "patterns": [ @@ -2099,6 +2124,33 @@ } ] }, + "subshell_dollar": { + "patterns": [ + { + "begin": "(?:\\$\\()", + "end": "\\)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.subshell.single.shell" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.subshell.single.shell" + } + }, + "name": "meta.scope.subshell", + "patterns": [ + { + "include": "#parenthese" + }, + { + "include": "#initial_context" + } + ] + } + ] + }, "support": { "patterns": [ { @@ -2107,6 +2159,37 @@ } ] }, + "typical_statements": { + "patterns": [ + { + "include": "#assignment_statement" + }, + { + "include": "#case_statement" + }, + { + "include": "#for_statement" + }, + { + "include": "#while_statement" + }, + { + "include": "#function_definition" + }, + { + "include": "#command_statement" + }, + { + "include": "#line_continuation" + }, + { + "include": "#arithmetic_double" + }, + { + "include": "#normal_context" + } + ] + }, "variable": { "patterns": [ { @@ -2161,19 +2244,10 @@ "contentName": "meta.parameter-expansion", "patterns": [ { - "match": "!|:[-=?]?|\\*|@|##|#|%%|%|\\/", - "name": "keyword.operator.expansion.shell" + "include": "#special_expansion" }, { - "match": "(?:(\\[)(?:[^\\]]+)(\\]))", - "captures": { - "1": { - "name": "punctuation.section.array.shell" - }, - "2": { - "name": "punctuation.section.array.shell" - } - } + "include": "#array_access_inline" }, { "match": "[0-9]+", @@ -2210,19 +2284,10 @@ "contentName": "meta.parameter-expansion", "patterns": [ { - "match": "!|:[-=?]?|\\*|@|##|#|%%|%|\\/", - "name": "keyword.operator.expansion.shell" + "include": "#special_expansion" }, { - "match": "(?:(\\[)(?:[^\\]]+)(\\]))", - "captures": { - "1": { - "name": "punctuation.section.array.shell" - }, - "2": { - "name": "punctuation.section.array.shell" - } - } + "include": "#array_access_inline" }, { "match": "(? ({ number: t.remoteAddress.port, privacy: t.privacy })); + const ports = [...this.tunnels].map(t => ({ number: t.remoteAddress.port, privacy: t.privacy, protocol: t.protocol })); this.state.process.stdin.write(`${JSON.stringify(ports)}\n`); if (ports.length === 0 && !this.state.cleanupTimeout) { @@ -259,12 +262,10 @@ class TunnelProvider implements vscode.TunnelProvider { 'forward-internal', '--provider', 'github', - '--access-token', - session.accessToken, ]; this.logger.log('info', '[forwarding] starting CLI'); - const child = spawn(cliPath, args, { stdio: 'pipe', env: { ...process.env, NO_COLOR: '1' } }); + const child = spawn(cliPath, args, { stdio: 'pipe', env: { ...process.env, NO_COLOR: '1', VSCODE_CLI_ACCESS_TOKEN: session.accessToken } }); this.state = { state: State.Starting, process: child }; const progressP = new DeferredPromise(); diff --git a/extensions/tunnel-forwarding/yarn.lock b/extensions/tunnel-forwarding/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/tunnel-forwarding/yarn.lock +++ b/extensions/tunnel-forwarding/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/typescript-basics/language-configuration.json b/extensions/typescript-basics/language-configuration.json index 070b8911a82..25a23685738 100644 --- a/extensions/typescript-basics/language-configuration.json +++ b/extensions/typescript-basics/language-configuration.json @@ -129,10 +129,10 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" + "pattern": "^\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { - "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" }, // e.g. * ...| or */| or *-----*/| "unIndentedLinePattern": { diff --git a/extensions/typescript-basics/snippets/typescript.code-snippets b/extensions/typescript-basics/snippets/typescript.code-snippets index 35b2aa1711c..9ed695795eb 100644 --- a/extensions/typescript-basics/snippets/typescript.code-snippets +++ b/extensions/typescript-basics/snippets/typescript.code-snippets @@ -163,7 +163,7 @@ "For-Of Loop": { "prefix": "forof", "body": [ - "for (const ${1:iterator} of ${2:object}) {", + "for (const ${1:element} of ${2:object}) {", "\t$TM_SELECTED_TEXT$0", "}" ], @@ -172,7 +172,7 @@ "For-Await-Of Loop": { "prefix": "forawaitof", "body": [ - "for await (const ${1:iterator} of ${2:object}) {", + "for await (const ${1:element} of ${2:object}) {", "\t$TM_SELECTED_TEXT$0", "}" ], @@ -266,6 +266,15 @@ ], "description": "Set Timeout Function" }, + "Set Interval Function": { + "prefix": "setinterval", + "body": [ + "setInterval(() => {", + "\t$TM_SELECTED_TEXT$0", + "}, ${1:interval});" + ], + "description": "Set Interval Function" + }, "Region Start": { "prefix": "#region", "body": [ diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index d14576b338d..9e721253f2a 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -9,10 +9,12 @@ "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "enabledApiProposals": [ "workspaceTrust", + "createFileSystemWatcher", "multiDocumentHighlightProvider", "mappedEditsProvider", "codeActionAI", - "codeActionRanges" + "codeActionRanges", + "documentPaste" ], "capabilities": { "virtualWorkspaces": { @@ -49,7 +51,7 @@ "vscode-uri": "^3.0.3" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/semver": "^5.5.0" }, "scripts": { @@ -139,6 +141,10 @@ "fileMatch": "jsconfig.*.json", "url": "./schemas/jsconfig.schema.json" }, + { + "fileMatch": ".swcrc", + "url": "https://swc.rs/schema.json" + }, { "fileMatch": "typedoc.json", "url": "https://typedoc.org/schema.json" @@ -1266,6 +1272,11 @@ "experimental" ] }, + "typescript.tsserver.experimental.useVsCodeWatcher": { + "type": "boolean", + "description": "%configuration.tsserver.useVsCodeWatcher%", + "default": true + }, "typescript.tsserver.watchOptions": { "type": "object", "description": "%configuration.tsserver.watchOptions%", @@ -1373,18 +1384,17 @@ "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", "scope": "window" }, + "typescript.tsserver.web.typeAcquisition.enabled": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.web.typeAcquisition.enabled%", + "scope": "window" + }, "typescript.tsserver.nodePath": { "type": "string", "description": "%configuration.tsserver.nodePath%", "scope": "window" }, - "typescript.experimental.tsserver.web.typeAcquisition.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.experimental.tsserver.web.typeAcquisition.enabled%", - "scope": "window", - "tags": ["experimental"] - }, "typescript.preferGoToSourceDefinition": { "type": "boolean", "default": false, @@ -1402,6 +1412,26 @@ "default": true, "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", "scope": "window" + }, + "typescript.tsserver.enableRegionDiagnostics": { + "type": "boolean", + "default": true, + "description": "%typescript.tsserver.enableRegionDiagnostics%", + "scope": "window" + }, + "javascript.experimental.updateImportsOnPaste": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "%configuration.updateImportsOnPaste%", + "tags": ["experimental"] + }, + "typescript.experimental.updateImportsOnPaste": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "%configuration.updateImportsOnPaste%", + "tags": ["experimental"] } } }, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index df741e51802..7a2a188dbd7 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -16,6 +16,7 @@ "typescript.tsserver.pluginPaths": "Additional paths to discover TypeScript Language Service plugins.", "typescript.tsserver.pluginPaths.item": "Either an absolute or relative path. Relative path will be resolved against workspace folder(s).", "typescript.tsserver.trace": "Enables tracing of messages sent to the TS server. This trace can be used to diagnose TS Server issues. The trace may contain file paths, source code, and other potentially sensitive information from your project.", + "typescript.tsserver.enableRegionDiagnostics": "Enables region-based diagnostics in TypeScript. Requires using TypeScript 5.6+ in the workspace.", "typescript.validate.enable": "Enable/disable TypeScript validation.", "typescript.format.enable": "Enable/disable default TypeScript formatter.", "javascript.format.enable": "Enable/disable default JavaScript formatter.", @@ -164,6 +165,7 @@ "typescript.suggest.enabled": "Enabled/disable autocomplete suggestions.", "configuration.surveys.enabled": "Enabled/disable occasional surveys that help us improve VS Code's JavaScript and TypeScript support.", "configuration.suggest.completeJSDocs": "Enable/disable suggestion to complete JSDoc comments.", + "configuration.tsserver.useVsCodeWatcher": "Use VS Code's file watchers instead of TypeScript's. Requires using TypeScript 5.4+ in the workspace.", "configuration.tsserver.watchOptions": "Configure which watching strategies should be used to keep track of files and directories.", "configuration.tsserver.watchOptions.watchFile": "Strategy for how individual files are watched.", "configuration.tsserver.watchOptions.watchFile.fixedChunkSizePolling": "Polls files in chunks at regular interval.", @@ -234,9 +236,10 @@ "configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members.", "configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals.", "configuration.tsserver.web.projectWideIntellisense.enabled": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.", - "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors. This is needed when using external packages as these can't be included analyzed on web.", + "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", + "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", - "configuration.experimental.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web.", + "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.6+.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/extensions/typescript-language-features/schemas/package.schema.json b/extensions/typescript-language-features/schemas/package.schema.json index c135ea39ec1..1cd2b1ed8ea 100644 --- a/extensions/typescript-language-features/schemas/package.schema.json +++ b/extensions/typescript-language-features/schemas/package.schema.json @@ -1,6 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "TypeScript contributions to package.json", "type": "object", "properties": { "contributes": { diff --git a/extensions/typescript-language-features/src/commands/commandManager.ts b/extensions/typescript-language-features/src/commands/commandManager.ts index ccc42b860b1..92b5158560c 100644 --- a/extensions/typescript-language-features/src/commands/commandManager.ts +++ b/extensions/typescript-language-features/src/commands/commandManager.ts @@ -12,19 +12,30 @@ export interface Command { } export class CommandManager { - private readonly commands = new Map(); + private readonly commands = new Map(); public dispose() { for (const registration of this.commands.values()) { - registration.dispose(); + registration.registration.dispose(); } this.commands.clear(); } - public register(command: T): T { - if (!this.commands.has(command.id)) { - this.commands.set(command.id, vscode.commands.registerCommand(command.id, command.execute, command)); + public register(command: T): vscode.Disposable { + let entry = this.commands.get(command.id); + if (!entry) { + entry = { refCount: 1, registration: vscode.commands.registerCommand(command.id, command.execute, command) }; + this.commands.set(command.id, entry); + } else { + entry.refCount += 1; } - return command; + + return new vscode.Disposable(() => { + entry.refCount -= 1; + if (entry.refCount <= 0) { + entry.registration.dispose(); + this.commands.delete(command.id); + } + }); } } diff --git a/extensions/typescript-language-features/src/commands/openJsDocLink.ts b/extensions/typescript-language-features/src/commands/openJsDocLink.ts index 10a480a4bd3..8535fb1c239 100644 --- a/extensions/typescript-language-features/src/commands/openJsDocLink.ts +++ b/extensions/typescript-language-features/src/commands/openJsDocLink.ts @@ -7,8 +7,17 @@ import * as vscode from 'vscode'; import { Command } from './commandManager'; export interface OpenJsDocLinkCommand_Args { - readonly file: vscode.Uri; - readonly position: vscode.Position; + readonly file: { + readonly scheme: string; + readonly authority?: string; + readonly path?: string; + readonly query?: string; + readonly fragment?: string; + }; + readonly position: { + readonly line: number; + readonly character: number; + }; } /** @@ -21,8 +30,10 @@ export class OpenJsDocLinkCommand implements Command { public readonly id = OpenJsDocLinkCommand.id; public async execute(args: OpenJsDocLinkCommand_Args): Promise { - await vscode.commands.executeCommand('vscode.open', vscode.Uri.from(args.file), { - selection: new vscode.Range(args.position, args.position), - }); + const { line, character } = args.position; + const position = new vscode.Position(line, character); + await vscode.commands.executeCommand('vscode.open', vscode.Uri.from(args.file), { + selection: new vscode.Range(position, position), + } satisfies vscode.TextDocumentShowOptions); } } diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index 5fce20d1d1f..639f3d346e0 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -112,17 +112,19 @@ export interface TypeScriptServiceConfiguration { readonly useSyntaxServer: SyntaxServerConfiguration; readonly webProjectWideIntellisenseEnabled: boolean; readonly webProjectWideIntellisenseSuppressSemanticErrors: boolean; - readonly webExperimentalTypeAcquisition: boolean; + readonly webTypeAcquisitionEnabled: boolean; readonly enableDiagnosticsTelemetry: boolean; readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; readonly enablePromptUseWorkspaceTsdk: boolean; + readonly useVsCodeWatcher: boolean; // TODO@bpasero remove this setting eventually readonly watchOptions: Proto.WatchOptions | undefined; readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined; readonly enableTsServerTracing: boolean; readonly localNodePath: string | null; readonly globalNodePath: string | null; readonly workspaceSymbolsExcludeLibrarySymbols: boolean; + readonly enableRegionDiagnostics: boolean; } export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean { @@ -149,17 +151,19 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu useSyntaxServer: this.readUseSyntaxServer(configuration), webProjectWideIntellisenseEnabled: this.readWebProjectWideIntellisenseEnable(configuration), webProjectWideIntellisenseSuppressSemanticErrors: this.readWebProjectWideIntellisenseSuppressSemanticErrors(configuration), - webExperimentalTypeAcquisition: this.readWebExperimentalTypeAcquisition(configuration), + webTypeAcquisitionEnabled: this.readWebTypeAcquisition(configuration), enableDiagnosticsTelemetry: this.readEnableDiagnosticsTelemetry(configuration), enableProjectDiagnostics: this.readEnableProjectDiagnostics(configuration), maxTsServerMemory: this.readMaxTsServerMemory(configuration), enablePromptUseWorkspaceTsdk: this.readEnablePromptUseWorkspaceTsdk(configuration), + useVsCodeWatcher: this.readUseVsCodeWatcher(configuration), watchOptions: this.readWatchOptions(configuration), includePackageJsonAutoImports: this.readIncludePackageJsonAutoImports(configuration), enableTsServerTracing: this.readEnableTsServerTracing(configuration), localNodePath: this.readLocalNodePath(configuration), globalNodePath: this.readGlobalNodePath(configuration), workspaceSymbolsExcludeLibrarySymbols: this.readWorkspaceSymbolsExcludeLibrarySymbols(configuration), + enableRegionDiagnostics: this.readEnableRegionDiagnostics(configuration), }; } @@ -185,10 +189,6 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.disableAutomaticTypeAcquisition', false); } - protected readWebExperimentalTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.experimental.tsserver.web.typeAcquisition.enabled', false); - } - protected readLocale(configuration: vscode.WorkspaceConfiguration): string | null { const value = configuration.get('typescript.locale', 'auto'); return !value || value === 'auto' ? null : value; @@ -222,7 +222,11 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.tsserver.experimental.enableProjectDiagnostics', false); } - protected readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined { + private readUseVsCodeWatcher(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.experimental.useVsCodeWatcher', false); + } + + private readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined { const watchOptions = configuration.get('typescript.tsserver.watchOptions'); // Returned value may be a proxy. Clone it into a normal object return { ...(watchOptions ?? {}) }; @@ -250,6 +254,10 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.tsserver.enableTracing', false); } + private readWorkspaceSymbolsExcludeLibrarySymbols(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.workspaceSymbols.excludeLibrarySymbols', true); + } + private readWebProjectWideIntellisenseEnable(configuration: vscode.WorkspaceConfiguration): boolean { return configuration.get('typescript.tsserver.web.projectWideIntellisense.enabled', true); } @@ -258,7 +266,11 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', true); } - private readWorkspaceSymbolsExcludeLibrarySymbols(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.workspaceSymbols.excludeLibrarySymbols', true); + private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.web.typeAcquisition.enabled', false); + } + + private readEnableRegionDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.enableRegionDiagnostics', true); } } diff --git a/extensions/typescript-language-features/src/configuration/fileSchemes.ts b/extensions/typescript-language-features/src/configuration/fileSchemes.ts index cfc3f66db17..ca268e29e0b 100644 --- a/extensions/typescript-language-features/src/configuration/fileSchemes.ts +++ b/extensions/typescript-language-features/src/configuration/fileSchemes.ts @@ -17,8 +17,13 @@ export const vsls = 'vsls'; export const walkThroughSnippet = 'walkThroughSnippet'; export const vscodeNotebookCell = 'vscode-notebook-cell'; export const officeScript = 'office-script'; + +/** Used for code blocks in chat by vs code core */ export const chatCodeBlock = 'vscode-chat-code-block'; +/** Used for code blocks in chat by copilot. */ +export const chatBackingCodeBlock = 'vscode-copilot-chat-code-block'; + export function getSemanticSupportedSchemes() { if (isWeb() && vscode.workspace.workspaceFolders) { return vscode.workspace.workspaceFolders.map(folder => folder.uri.scheme); @@ -30,6 +35,7 @@ export function getSemanticSupportedSchemes() { walkThroughSnippet, vscodeNotebookCell, chatCodeBlock, + chatBackingCodeBlock, ]; } @@ -42,3 +48,8 @@ export const disabledSchemes = new Set([ github, azurerepos, ]); + +export function isOfScheme(uri: vscode.Uri, ...schemes: string[]): boolean { + const normalizedUriScheme = uri.scheme.toLowerCase(); + return schemes.some(scheme => normalizedUriScheme === scheme); +} diff --git a/extensions/typescript-language-features/src/configuration/languageDescription.ts b/extensions/typescript-language-features/src/configuration/languageDescription.ts index 95c165bf14d..a97530e5ac4 100644 --- a/extensions/typescript-language-features/src/configuration/languageDescription.ts +++ b/extensions/typescript-language-features/src/configuration/languageDescription.ts @@ -32,7 +32,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ diagnosticSource: 'ts', diagnosticLanguage: DiagnosticLanguage.TypeScript, languageIds: [languageIds.typescript, languageIds.typescriptreact], - configFilePattern: /^tsconfig(\..*)?\.json$/gi, + configFilePattern: /^tsconfig(\..*)?\.json$/i, standardFileExtensions: [ 'ts', 'tsx', @@ -45,7 +45,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ diagnosticSource: 'ts', diagnosticLanguage: DiagnosticLanguage.JavaScript, languageIds: [languageIds.javascript, languageIds.javascriptreact], - configFilePattern: /^jsconfig(\..*)?\.json$/gi, + configFilePattern: /^jsconfig(\..*)?\.json$/i, standardFileExtensions: [ 'js', 'jsx', diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 66532bc81fc..9ad0867efc9 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -11,8 +11,7 @@ import { registerBaseCommands } from './commands/index'; import { TypeScriptServiceConfiguration } from './configuration/configuration'; import { BrowserServiceConfigurationProvider } from './configuration/configuration.browser'; import { ExperimentationTelemetryReporter, IExperimentationTelemetryReporter } from './experimentTelemetryReporter'; -import { AutoInstallerFs } from './filesystems/autoInstallerFs'; -import { MemFs } from './filesystems/memFs'; +import { registerAtaSupport } from './filesystems/ata'; import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost'; import { Logger } from './logging/logger'; import RemoteRepositories from './remoteRepositories.browser'; @@ -62,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { new TypeScriptVersion( TypeScriptVersionSource.Bundled, vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(), - API.fromSimpleString('5.3.2'))); + API.fromSimpleString('5.5.4'))); let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; const packageInfo = getPackageInfo(context); @@ -101,14 +100,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager, activeJsTsEditorTracker, async () => { await startPreloadWorkspaceContentsIfNeeded(context, logger); })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { - isCaseSensitive: true, - isReadonly: false - })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { - isCaseSensitive: true, - isReadonly: false - })); + + context.subscriptions.push(registerAtaSupport(logger)); return getExtensionApi(onCompletionAccepted.event, pluginManager); } @@ -118,15 +111,25 @@ async function startPreloadWorkspaceContentsIfNeeded(context: vscode.ExtensionCo return; } - const workspaceUri = vscode.workspace.workspaceFolders?.[0].uri; - if (!workspaceUri || workspaceUri.scheme !== 'vscode-vfs' || !workspaceUri.authority.startsWith('github')) { - logger.info(`Skipped loading workspace contents for repository ${workspaceUri?.toString()}`); + if (!vscode.workspace.workspaceFolders) { return; } - const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger); - context.subscriptions.push(loader); - return loader.triggerPreload(); + await Promise.all(vscode.workspace.workspaceFolders.map(async folder => { + const workspaceUri = folder.uri; + if (workspaceUri.scheme !== 'vscode-vfs' || !workspaceUri.authority.startsWith('github')) { + logger.info(`Skipped pre loading workspace contents for repository ${workspaceUri?.toString()}`); + return; + } + + const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger); + context.subscriptions.push(loader); + try { + await loader.triggerPreload(); + } catch (error) { + console.error(error); + } + })); } class RemoteWorkspaceContentsPreloader extends Disposable { diff --git a/extensions/typescript-language-features/src/filesystems/ata.ts b/extensions/typescript-language-features/src/filesystems/ata.ts new file mode 100644 index 00000000000..b5e43244e1b --- /dev/null +++ b/extensions/typescript-language-features/src/filesystems/ata.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { conditionalRegistration, requireGlobalConfiguration } from '../languageFeatures/util/dependentRegistration'; +import { supportsReadableByteStreams } from '../utils/platform'; +import { AutoInstallerFs } from './autoInstallerFs'; +import { MemFs } from './memFs'; +import { Logger } from '../logging/logger'; + +export function registerAtaSupport(logger: Logger): vscode.Disposable { + if (!supportsReadableByteStreams()) { + return vscode.Disposable.from(); + } + + return conditionalRegistration([ + requireGlobalConfiguration('typescript', 'tsserver.web.typeAcquisition.enabled'), + ], () => { + return vscode.Disposable.from( + // Ata + vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs('global-typings', logger), { + isCaseSensitive: true, + isReadonly: false, + }), + + // Read accesses to node_modules + vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(logger), { + isCaseSensitive: true, + isReadonly: false + })); + }); +} diff --git a/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts b/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts index 4e69fce8cda..c26476a983d 100644 --- a/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts +++ b/extensions/typescript-language-features/src/filesystems/autoInstallerFs.ts @@ -3,50 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { PackageManager } from '@vscode/ts-package-manager'; +import { basename, join } from 'path'; import * as vscode from 'vscode'; -import { MemFs } from './memFs'; import { URI } from 'vscode-uri'; -import { PackageManager, FileSystem, packagePath } from '@vscode/ts-package-manager'; -import { join, basename, dirname } from 'path'; +import { Throttler } from '../utils/async'; +import { Disposable } from '../utils/dispose'; +import { MemFs } from './memFs'; +import { Logger } from '../logging/logger'; const TEXT_DECODER = new TextDecoder('utf-8'); const TEXT_ENCODER = new TextEncoder(); -export class AutoInstallerFs implements vscode.FileSystemProvider { +export class AutoInstallerFs extends Disposable implements vscode.FileSystemProvider { - private readonly memfs = new MemFs(); - private readonly fs: FileSystem; - private readonly projectCache = new Map>(); - private readonly watcher: vscode.FileSystemWatcher; - private readonly _emitter = new vscode.EventEmitter(); + private readonly memfs: MemFs; + private readonly packageManager: PackageManager; + private readonly _projectCache = new Map(); - readonly onDidChangeFile: vscode.Event = this._emitter.event; + private readonly _emitter = this._register(new vscode.EventEmitter()); + readonly onDidChangeFile = this._emitter.event; - constructor() { - this.watcher = vscode.workspace.createFileSystemWatcher('**/{package.json,package-lock.json,package-lock.kdl}'); - const handler = (uri: URI) => { - const root = dirname(uri.path); - if (this.projectCache.delete(root)) { - (async () => { - const pm = new PackageManager(this.fs); - const opts = await this.getInstallOpts(uri, root); - const proj = await pm.resolveProject(root, opts); - proj.pruneExtraneous(); - // TODO: should this fire on vscode-node-modules instead? - // NB(kmarchan): This should tell TSServer that there's - // been changes inside node_modules and it needs to - // re-evaluate things. - this._emitter.fire([{ - type: vscode.FileChangeType.Changed, - uri: uri.with({ path: join(root, 'node_modules') }) - }]); - })(); - } - }; - this.watcher.onDidChange(handler); - this.watcher.onDidCreate(handler); - this.watcher.onDidDelete(handler); - const memfs = this.memfs; + constructor( + private readonly logger: Logger + ) { + super(); + + const memfs = new MemFs('auto-installer', logger); + this.memfs = memfs; memfs.onDidChangeFile((e) => { this._emitter.fire(e.map(ev => ({ type: ev.type, @@ -54,7 +40,8 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { uri: ev.uri.with({ scheme: 'memfs' }) }))); }); - this.fs = { + + this.packageManager = new PackageManager({ readDirectory(path: string, _extensions?: readonly string[], _exclude?: readonly string[], _include?: readonly string[], _depth?: number): string[] { return memfs.readDirectory(URI.file(path)).map(([name, _]) => name); }, @@ -87,17 +74,18 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { return undefined; } } - }; + }); } watch(resource: vscode.Uri): vscode.Disposable { const mapped = URI.file(new MappedUri(resource).path); - console.log('watching', mapped); + this.logger.trace(`AutoInstallerFs.watch. Original: ${resource.toString()}, Mapped: ${mapped.toString()}`); return this.memfs.watch(mapped); } async stat(uri: vscode.Uri): Promise { - // console.log('stat', uri.toString()); + this.logger.trace(`AutoInstallerFs.stat: ${uri}`); + const mapped = new MappedUri(uri); // TODO: case sensitivity configuration @@ -119,7 +107,8 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { - // console.log('readDirectory', uri.toString()); + this.logger.trace(`AutoInstallerFs.readDirectory: ${uri}`); + const mapped = new MappedUri(uri); await this.ensurePackageContents(mapped); @@ -127,7 +116,8 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } async readFile(uri: vscode.Uri): Promise { - // console.log('readFile', uri.toString()); + this.logger.trace(`AutoInstallerFs.readFile: ${uri}`); + const mapped = new MappedUri(uri); await this.ensurePackageContents(mapped); @@ -151,8 +141,6 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } private async ensurePackageContents(incomingUri: MappedUri): Promise { - // console.log('ensurePackageContents', incomingUri.path); - // If we're not looking for something inside node_modules, bail early. if (!incomingUri.path.includes('node_modules')) { throw vscode.FileSystemError.FileNotFound(); @@ -164,25 +152,27 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { } const root = this.getProjectRoot(incomingUri.path); - - const pkgPath = packagePath(incomingUri.path); - if (!root || this.projectCache.get(root)?.has(pkgPath)) { + if (!root) { return; } - const proj = await (new PackageManager(this.fs)).resolveProject(root, await this.getInstallOpts(incomingUri.original, root)); + this.logger.trace(`AutoInstallerFs.ensurePackageContents. Path: ${incomingUri.path}, Root: ${root}`); - const restore = proj.restorePackageAt(incomingUri.path); - try { - await restore; - } catch (e) { - console.error(`failed to restore package at ${incomingUri.path}: `, e); - throw e; + let projectEntry = this._projectCache.get(root); + if (!projectEntry) { + projectEntry = { throttler: new Throttler() }; + this._projectCache.set(root, projectEntry); } - if (!this.projectCache.has(root)) { - this.projectCache.set(root, new Set()); - } - this.projectCache.get(root)!.add(pkgPath); + + projectEntry.throttler.queue(async () => { + const proj = await this.packageManager.resolveProject(root, await this.getInstallOpts(incomingUri.original, root)); + try { + await proj.restore(); + } catch (e) { + console.error(`failed to restore package at ${incomingUri.path}: `, e); + throw e; + } + }); } private async getInstallOpts(originalUri: URI, root: string) { @@ -213,9 +203,6 @@ export class AutoInstallerFs implements vscode.FileSystemProvider { const pkgPath = path.match(/(^.*)\/node_modules/); return pkgPath?.[1]; } - - // --- manage file events - } class MappedUri { diff --git a/extensions/typescript-language-features/src/filesystems/memFs.ts b/extensions/typescript-language-features/src/filesystems/memFs.ts index 02476ec1804..05c4e7c3db7 100644 --- a/extensions/typescript-language-features/src/filesystems/memFs.ts +++ b/extensions/typescript-language-features/src/filesystems/memFs.ts @@ -3,19 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import { basename, dirname } from 'path'; +import * as vscode from 'vscode'; +import { Logger } from '../logging/logger'; export class MemFs implements vscode.FileSystemProvider { - private readonly root = new FsEntry( + private readonly root = new FsDirectoryEntry( new Map(), 0, 0, ); + constructor( + private readonly id: string, + private readonly logger: Logger, + ) { } + stat(uri: vscode.Uri): vscode.FileStat { - // console.log('stat', uri.toString()); + this.logger.trace(`MemFs.stat ${this.id}. uri: ${uri}`); const entry = this.getEntry(uri); if (!entry) { throw vscode.FileSystemError.FileNotFound(); @@ -25,29 +31,36 @@ export class MemFs implements vscode.FileSystemProvider { } readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { - // console.log('readDirectory', uri.toString()); + this.logger.trace(`MemFs.readDirectory ${this.id}. uri: ${uri}`); + + const entry = this.getEntry(uri); + if (!entry) { + throw vscode.FileSystemError.FileNotFound(); + } + if (!(entry instanceof FsDirectoryEntry)) { + throw vscode.FileSystemError.FileNotADirectory(); + } + + return Array.from(entry.contents.entries(), ([name, entry]) => [name, entry.type]); + } + + readFile(uri: vscode.Uri): Uint8Array { + this.logger.trace(`MemFs.readFile ${this.id}. uri: ${uri}`); const entry = this.getEntry(uri); if (!entry) { throw vscode.FileSystemError.FileNotFound(); } - return [...entry.contents.entries()].map(([name, entry]) => [name, entry.type]); - } - - readFile(uri: vscode.Uri): Uint8Array { - // console.log('readFile', uri.toString()); - - const entry = this.getEntry(uri); - if (!entry) { - throw vscode.FileSystemError.FileNotFound(); + if (!(entry instanceof FsFileEntry)) { + throw vscode.FileSystemError.FileIsADirectory(uri); } return entry.data; } writeFile(uri: vscode.Uri, content: Uint8Array, { create, overwrite }: { create: boolean; overwrite: boolean }): void { - // console.log('writeFile', uri.toString()); + this.logger.trace(`MemFs.writeFile ${this.id}. uri: ${uri}`); const dir = this.getParent(uri); @@ -58,12 +71,16 @@ export class MemFs implements vscode.FileSystemProvider { const entry = dirContents.get(basename(uri.path)); if (!entry) { if (create) { - dirContents.set(fileName, new FsEntry(content, time, time)); + dirContents.set(fileName, new FsFileEntry(content, time, time)); this._emitter.fire([{ type: vscode.FileChangeType.Created, uri }]); } else { throw vscode.FileSystemError.FileNotFound(); } } else { + if (entry instanceof FsDirectoryEntry) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (overwrite) { entry.mtime = time; entry.data = content; @@ -87,13 +104,14 @@ export class MemFs implements vscode.FileSystemProvider { } createDirectory(uri: vscode.Uri): void { - // console.log('createDirectory', uri.toString()); + this.logger.trace(`MemFs.createDirectory ${this.id}. uri: ${uri}`); + const dir = this.getParent(uri); const now = Date.now() / 1000; - dir.contents.set(basename(uri.path), new FsEntry(new Map(), now, now)); + dir.contents.set(basename(uri.path), new FsDirectoryEntry(new Map(), now, now)); } - private getEntry(uri: vscode.Uri): FsEntry | void { + private getEntry(uri: vscode.Uri): FsEntry | undefined { // TODO: have this throw FileNotFound itself? // TODO: support configuring case sensitivity let node: FsEntry = this.root; @@ -104,13 +122,12 @@ export class MemFs implements vscode.FileSystemProvider { continue; } - if (node.type !== vscode.FileType.Directory) { + if (!(node instanceof FsDirectoryEntry)) { // We're looking at a File or such, so bail. return; } const next = node.contents.get(component); - if (!next) { // not found! return; @@ -121,11 +138,14 @@ export class MemFs implements vscode.FileSystemProvider { return node; } - private getParent(uri: vscode.Uri) { + private getParent(uri: vscode.Uri): FsDirectoryEntry { const dir = this.getEntry(uri.with({ path: dirname(uri.path) })); if (!dir) { throw vscode.FileSystemError.FileNotFound(); } + if (!(dir instanceof FsDirectoryEntry)) { + throw vscode.FileSystemError.FileNotADirectory(); + } return dir; } @@ -153,46 +173,32 @@ export class MemFs implements vscode.FileSystemProvider { } } -class FsEntry { - get type(): vscode.FileType { - if (this._data instanceof Uint8Array) { - return vscode.FileType.File; - } else { - return vscode.FileType.Directory; - } - } +class FsFileEntry { + readonly type = vscode.FileType.File; get size(): number { - if (this.type === vscode.FileType.Directory) { - return [...this.contents.values()].reduce((acc: number, entry: FsEntry) => acc + entry.size, 0); - } else { - return this.data.length; - } + return this.data.length; } constructor( - private _data: Uint8Array | Map, - public ctime: number, + public data: Uint8Array, + public readonly ctime: number, public mtime: number, ) { } - - get data() { - if (this.type === vscode.FileType.Directory) { - throw vscode.FileSystemError.FileIsADirectory; - } - return this._data; - } - set data(val: Uint8Array) { - if (this.type === vscode.FileType.Directory) { - throw vscode.FileSystemError.FileIsADirectory; - } - this._data = val; - } - - get contents() { - if (this.type !== vscode.FileType.Directory) { - throw vscode.FileSystemError.FileNotADirectory; - } - return >this._data; - } } + +class FsDirectoryEntry { + readonly type = vscode.FileType.Directory; + + get size(): number { + return [...this.contents.values()].reduce((acc: number, entry: FsEntry) => acc + entry.size, 0); + } + + constructor( + public readonly contents: Map, + public readonly ctime: number, + public readonly mtime: number, + ) { } +} + +type FsEntry = FsFileEntry | FsDirectoryEntry; diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index a46835aa907..038fb447da9 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -58,6 +58,7 @@ class MyCompletionItem extends vscode.CompletionItem { private readonly completionContext: CompletionContext, public readonly metadata: any | undefined, client: ITypeScriptServiceClient, + defaultCommitCharacters: readonly string[] | undefined, ) { const label = tsEntry.name || (tsEntry.insertText ?? ''); super(label, MyCompletionItem.convertKind(tsEntry.kind)); @@ -93,7 +94,7 @@ class MyCompletionItem extends vscode.CompletionItem { this.useCodeSnippet = completionContext.completeFunctionCalls && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method); this.range = this.getRangeFromReplacementSpan(tsEntry, completionContext); - this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry); + this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry, defaultCommitCharacters); this.insertText = isSnippet && tsEntry.insertText ? new vscode.SnippetString(tsEntry.insertText) : tsEntry.insertText; this.filterText = tsEntry.filterText || this.getFilterText(completionContext.line, tsEntry.insertText); @@ -188,7 +189,7 @@ class MyCompletionItem extends vscode.CompletionItem { ] }; const response = await client.interruptGetErr(() => client.execute('completionEntryDetails', args, requestToken.token)); - if (response.type !== 'response' || !response.body || !response.body.length) { + if (response.type !== 'response' || !response.body?.length) { return undefined; } @@ -500,7 +501,22 @@ class MyCompletionItem extends vscode.CompletionItem { } } - private static getCommitCharacters(context: CompletionContext, entry: Proto.CompletionEntry): string[] | undefined { + private static getCommitCharacters( + context: CompletionContext, + entry: Proto.CompletionEntry, + defaultCommitCharacters: readonly string[] | undefined): string[] | undefined { + // @ts-expect-error until TS 5.6 + let commitCharacters = entry.commitCharacters ?? defaultCommitCharacters; + if (commitCharacters) { + if (context.enableCallCompletions + && !context.isNewIdentifierLocation + && entry.kind !== PConst.Kind.warning + && entry.kind !== PConst.Kind.string) { + commitCharacters.push('('); + } + return commitCharacters; + } + if (entry.kind === PConst.Kind.warning || entry.kind === PConst.Kind.string) { // Ambient JS word based suggestion, strings return undefined; } @@ -509,7 +525,7 @@ class MyCompletionItem extends vscode.CompletionItem { return undefined; } - const commitCharacters: string[] = ['.', ',', ';']; + commitCharacters = ['.', ',', ';']; if (context.enableCallCompletions) { commitCharacters.push('('); } @@ -744,6 +760,7 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< let response: ServerResponse.Response | undefined; let duration: number | undefined; let optionalReplacementRange: vscode.Range | undefined; + let defaultCommitCharacters: string[] | undefined; if (this.client.apiVersion.gte(API.v300)) { const startTime = Date.now(); try { @@ -766,9 +783,11 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< dotAccessorContext = { range, text }; } } - isIncomplete = !!response.body.isIncomplete || (response as any).metadata && (response as any).metadata.isIncomplete; + isIncomplete = !!response.body.isIncomplete || (response.metadata as any)?.isIncomplete; entries = response.body.entries; metadata = response.metadata; + // @ts-expect-error until TS 5.6 + defaultCommitCharacters = response.body.defaultCommitCharacters; if (response.body.optionalReplacementSpan) { optionalReplacementRange = typeConverters.Range.fromTextSpan(response.body.optionalReplacementSpan); @@ -799,7 +818,14 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< const items: MyCompletionItem[] = []; for (const entry of entries) { if (!shouldExcludeCompletionEntry(entry, completionConfiguration)) { - const item = new MyCompletionItem(position, document, entry, completionContext, metadata, this.client); + const item = new MyCompletionItem( + position, + document, + entry, + completionContext, + metadata, + this.client, + defaultCommitCharacters); item.command = { command: ApplyCompletionCommand.ID, title: '', diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts new file mode 100644 index 00000000000..3e2e61a8a52 --- /dev/null +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { DocumentSelector } from '../configuration/documentSelector'; +import * as typeConverters from '../typeConverters'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireGlobalConfiguration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; +import protocol from '../tsServer/protocol/protocol'; +import { API } from '../tsServer/api'; +import { LanguageDescription } from '../configuration/languageDescription'; + +class CopyMetadata { + constructor( + readonly resource: vscode.Uri, + readonly ranges: readonly vscode.Range[], + ) { } + + toJSON() { + return JSON.stringify({ + resource: this.resource.toJSON(), + ranges: this.ranges, + }); + } + + static fromJSON(str: string): CopyMetadata | undefined { + try { + const parsed = JSON.parse(str); + return new CopyMetadata( + vscode.Uri.from(parsed.resource), + parsed.ranges.map((r: any) => new vscode.Range(r[0].line, r[0].character, r[1].line, r[1].character))); + } catch { + // ignore + } + return undefined; + } +} + +const settingId = 'experimental.updateImportsOnPaste'; + +class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { + + static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'jsts', 'pasteWithImports'); + static readonly metadataMimeType = 'application/vnd.code.jsts.metadata'; + + constructor( + private readonly _modeId: string, + private readonly _client: ITypeScriptServiceClient, + ) { } + + prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken) { + dataTransfer.set(DocumentPasteProvider.metadataMimeType, + new vscode.DataTransferItem(new CopyMetadata(document.uri, ranges).toJSON())); + } + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + ranges: readonly vscode.Range[], + dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, + token: vscode.CancellationToken, + ): Promise { + const config = vscode.workspace.getConfiguration(this._modeId, document.uri); + if (!config.get(settingId, false)) { + return; + } + + const file = this._client.toOpenTsFilePath(document); + if (!file) { + return; + } + + const text = await dataTransfer.get('text/plain')?.asString(); + if (!text || token.isCancellationRequested) { + return; + } + + // Get optional metadata + const metadata = await this.extractMetadata(dataTransfer, token); + if (token.isCancellationRequested) { + return; + } + + let copiedFrom: { + file: string; + spans: protocol.TextSpan[]; + } | undefined; + if (metadata) { + const spans = metadata.ranges.map(typeConverters.Range.toTextSpan); + const copyFile = this._client.toTsFilePath(metadata.resource); + if (copyFile) { + copiedFrom = { file: copyFile, spans }; + } + } + + if (copiedFrom?.file === file) { + return; + } + + const response = await this._client.interruptGetErr(() => this._client.execute('getPasteEdits', { + file, + // TODO: only supports a single paste for now + pastedText: [text], + pasteLocations: ranges.map(typeConverters.Range.toTextSpan), + copiedFrom + }, token)); + if (response.type !== 'response' || !response.body?.edits.length || token.isCancellationRequested) { + return; + } + + const edit = new vscode.DocumentPasteEdit('', vscode.l10n.t("Paste with imports"), DocumentPasteProvider.kind); + const additionalEdit = new vscode.WorkspaceEdit(); + for (const edit of response.body.edits) { + additionalEdit.set(this._client.toResource(edit.fileName), edit.textChanges.map(typeConverters.TextEdit.fromCodeEdit)); + } + edit.additionalEdit = additionalEdit; + return [edit]; + } + + private async extractMetadata(dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + const metadata = await dataTransfer.get(DocumentPasteProvider.metadataMimeType)?.asString(); + if (token.isCancellationRequested) { + return undefined; + } + + return metadata ? CopyMetadata.fromJSON(metadata) : undefined; + } +} + +export function register(selector: DocumentSelector, language: LanguageDescription, client: ITypeScriptServiceClient) { + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.Semantic), + requireMinVersion(client, API.v560), + requireGlobalConfiguration(language.id, settingId), + ], () => { + return vscode.languages.registerDocumentPasteEditProvider(selector.semantic, new DocumentPasteProvider(language.id, client), { + providedPasteEditKinds: [DocumentPasteProvider.kind], + copyMimeTypes: [DocumentPasteProvider.metadataMimeType], + pasteMimeTypes: ['text/plain'], + }); + }); +} diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 450ffcb886c..032b2467674 100644 --- a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -11,6 +11,8 @@ import { ResourceMap } from '../utils/resourceMap'; import { TelemetryReporter } from '../logging/telemetry'; import { TypeScriptServiceConfiguration } from '../configuration/configuration'; import { equals } from '../utils/objects'; +// @ts-expect-error until ts 5.6 +import { DiagnosticPerformanceData as TsDiagnosticPerformanceData } from '../tsServer/protocol/protocol'; function diagnosticsEquals(a: vscode.Diagnostic, b: vscode.Diagnostic): boolean { if (a === b) { @@ -34,6 +36,7 @@ export const enum DiagnosticKind { Syntax, Semantic, Suggestion, + RegionSemantic, } class FileDiagnostics { @@ -48,7 +51,8 @@ class FileDiagnostics { public updateDiagnostics( language: DiagnosticLanguage, kind: DiagnosticKind, - diagnostics: ReadonlyArray + diagnostics: ReadonlyArray, + ranges: ReadonlyArray | undefined ): boolean { if (language !== this.language) { this._diagnostics.clear(); @@ -61,6 +65,9 @@ class FileDiagnostics { return false; } + if (kind === DiagnosticKind.RegionSemantic) { + return this.updateRegionDiagnostics(diagnostics, ranges!); + } this._diagnostics.set(kind, diagnostics); return true; } @@ -83,6 +90,23 @@ class FileDiagnostics { } } + /** + * @param ranges The ranges whose diagnostics were updated. + */ + private updateRegionDiagnostics( + diagnostics: ReadonlyArray, + ranges: ReadonlyArray): boolean { + if (!this._diagnostics.get(DiagnosticKind.Semantic)) { + this._diagnostics.set(DiagnosticKind.Semantic, diagnostics); + return true; + } + const oldDiagnostics = this._diagnostics.get(DiagnosticKind.Semantic)!; + const newDiagnostics = oldDiagnostics.filter(diag => !ranges.some(range => diag.range.intersection(range))); + newDiagnostics.push(...diagnostics); + this._diagnostics.set(DiagnosticKind.Semantic, newDiagnostics); + return true; + } + private getSuggestionDiagnostics(settings: DiagnosticSettings) { const enableSuggestions = settings.getEnableSuggestions(this.language); return this.get(DiagnosticKind.Suggestion).filter(x => { @@ -151,12 +175,16 @@ class DiagnosticSettings { } } +interface DiagnosticPerformanceData extends TsDiagnosticPerformanceData { + fileLineCount?: number; +} + class DiagnosticsTelemetryManager extends Disposable { private readonly _diagnosticCodesMap = new Map(); private readonly _diagnosticSnapshotsMap = new ResourceMap(uri => uri.toString(), { onCaseInsensitiveFileSystem: false }); private _timeout: NodeJS.Timeout | undefined; - private _telemetryEmitter: NodeJS.Timer | undefined; + private _telemetryEmitter: NodeJS.Timeout | undefined; constructor( private readonly _telemetryReporter: TelemetryReporter, @@ -172,6 +200,37 @@ class DiagnosticsTelemetryManager extends Disposable { this._registerTelemetryEventEmitter(); } + public logDiagnosticsPerformanceTelemetry(performanceData: DiagnosticPerformanceData[]): void { + for (const data of performanceData) { + /* __GDPR__ + "diagnostics.performance" : { + "owner": "mjbvz", + "syntaxDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "semanticDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "suggestionDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "regionSemanticDiagDuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "fileLineCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this._telemetryReporter.logTelemetry('diagnostics.performance', + { + // @ts-expect-error until ts 5.6 + syntaxDiagDuration: data.syntaxDiag, + // @ts-expect-error until ts 5.6 + semanticDiagDuration: data.semanticDiag, + // @ts-expect-error until ts 5.6 + suggestionDiagDuration: data.suggestionDiag, + // @ts-expect-error until ts 5.6 + regionSemanticDiagDuration: data.regionSemanticDiag, + fileLineCount: data.fileLineCount, + }, + ); + } + } + private _updateAllDiagnosticCodesAfterTimeout() { clearTimeout(this._timeout); this._timeout = setTimeout(() => this._updateDiagnosticCodes(), 5000); @@ -235,6 +294,8 @@ export class DiagnosticsManager extends Disposable { private readonly _updateDelay = 50; + private readonly _diagnosticsTelemetryManager: DiagnosticsTelemetryManager | undefined; + constructor( owner: string, configuration: TypeScriptServiceConfiguration, @@ -248,7 +309,7 @@ export class DiagnosticsManager extends Disposable { this._currentDiagnostics = this._register(vscode.languages.createDiagnosticCollection(owner)); // Here we are selecting only 1 user out of 1000 to send telemetry diagnostics if (Math.random() * 1000 <= 1 || configuration.enableDiagnosticsTelemetry) { - this._register(new DiagnosticsTelemetryManager(telemetryReporter, this._currentDiagnostics)); + this._diagnosticsTelemetryManager = this._register(new DiagnosticsTelemetryManager(telemetryReporter, this._currentDiagnostics)); } } @@ -284,15 +345,16 @@ export class DiagnosticsManager extends Disposable { file: vscode.Uri, language: DiagnosticLanguage, kind: DiagnosticKind, - diagnostics: ReadonlyArray + diagnostics: ReadonlyArray, + ranges: ReadonlyArray | undefined, ): void { let didUpdate = false; const entry = this._diagnostics.get(file); if (entry) { - didUpdate = entry.updateDiagnostics(language, kind, diagnostics); + didUpdate = entry.updateDiagnostics(language, kind, diagnostics, ranges); } else if (diagnostics.length) { const fileDiagnostics = new FileDiagnostics(file, language); - fileDiagnostics.updateDiagnostics(language, kind, diagnostics); + fileDiagnostics.updateDiagnostics(language, kind, diagnostics, ranges); this._diagnostics.set(file, fileDiagnostics); didUpdate = true; } @@ -326,6 +388,10 @@ export class DiagnosticsManager extends Disposable { return this._currentDiagnostics.get(file) || []; } + public logDiagnosticsPerformanceTelemetry(performanceData: DiagnosticPerformanceData[]): void { + this._diagnosticsTelemetryManager?.logDiagnosticsPerformanceTelemetry(performanceData); + } + private scheduleDiagnosticsUpdate(file: vscode.Uri) { if (!this._pendingUpdates.has(file)) { this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay)); diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 7fa9805a543..bddd062b3e8 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -191,13 +191,13 @@ export default class FileConfigurationManager extends Disposable { includeCompletionsWithClassMemberSnippets: config.get('suggest.classMemberSnippets.enabled', true), includeCompletionsWithObjectLiteralMethodSnippets: config.get('suggest.objectLiteralMethodSnippets.enabled', true), autoImportFileExcludePatterns: this.getAutoImportFileExcludePatternsPreference(preferencesConfig, vscode.workspace.getWorkspaceFolder(document.uri)?.uri), - // @ts-expect-error until 5.3 #56090 preferTypeOnlyAutoImports: preferencesConfig.get('preferTypeOnlyAutoImports', false), useLabelDetailsInCompletionEntries: true, allowIncompleteCompletions: true, displayPartsForJSDoc: true, disableLineTextInReferences: true, interactiveInlayHints: true, + includeCompletionsForModuleExports: config.get('suggest.autoImports'), ...getInlayHintsPreferences(config), }; diff --git a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts b/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts index 0d04d2dc808..06ce5557b6c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts +++ b/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts @@ -27,8 +27,8 @@ class TsMappedEditsProvider implements vscode.MappedEditsProvider { } const response = await this.client.execute('mapCode', { - mappings: [{ - file, + file, + mapping: { contents: codeBlocks, focusLocations: context.documents.map(documents => { return documents.flatMap((contextItem): FileSpan[] => { @@ -39,7 +39,7 @@ class TsMappedEditsProvider implements vscode.MappedEditsProvider { return contextItem.ranges.map((range): FileSpan => ({ file, ...Range.toTextSpan(range) })); }); }), - }], + } }, token); if (response.type !== 'response' || !response.body) { return; diff --git a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts index 1092e1f202b..82199438563 100644 --- a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts +++ b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts @@ -18,7 +18,7 @@ import { conditionalRegistration, requireMinVersion, requireSomeCapability } fro interface OrganizeImportsCommandMetadata { - readonly ids: readonly string[]; + readonly commandIds: readonly string[]; readonly title: string; readonly minVersion?: API; readonly kind: vscode.CodeActionKind; @@ -26,14 +26,14 @@ interface OrganizeImportsCommandMetadata { } const organizeImportsCommand: OrganizeImportsCommandMetadata = { - ids: ['typescript.organizeImports'], + commandIds: [], // We use the generic 'Organize imports' command title: vscode.l10n.t("Organize Imports"), kind: vscode.CodeActionKind.SourceOrganizeImports, mode: OrganizeImportsMode.All, }; const sortImportsCommand: OrganizeImportsCommandMetadata = { - ids: ['typescript.sortImports', 'javascript.sortImports'], + commandIds: ['typescript.sortImports', 'javascript.sortImports'], minVersion: API.v430, title: vscode.l10n.t("Sort Imports"), kind: vscode.CodeActionKind.Source.append('sortImports'), @@ -41,23 +41,23 @@ const sortImportsCommand: OrganizeImportsCommandMetadata = { }; const removeUnusedImportsCommand: OrganizeImportsCommandMetadata = { - ids: ['typescript.removeUnusedImports', 'javascript.removeUnusedImports'], + commandIds: ['typescript.removeUnusedImports', 'javascript.removeUnusedImports'], minVersion: API.v490, title: vscode.l10n.t("Remove Unused Imports"), kind: vscode.CodeActionKind.Source.append('removeUnusedImports'), mode: OrganizeImportsMode.RemoveUnused, }; -class OrganizeImportsCommand implements Command { +class DidOrganizeImportsCommand implements Command { + + public static readonly ID = '_typescript.didOrganizeImports'; + public readonly id = DidOrganizeImportsCommand.ID; constructor( - public readonly id: string, - private readonly commandMetadata: OrganizeImportsCommandMetadata, - private readonly client: ITypeScriptServiceClient, private readonly telemetryReporter: TelemetryReporter, ) { } - public async execute(file?: string): Promise { + public async execute(): Promise { /* __GDPR__ "organizeImports.execute" : { "owner": "mjbvz", @@ -67,48 +67,20 @@ class OrganizeImportsCommand implements Command { } */ this.telemetryReporter.logTelemetry('organizeImports.execute', {}); - if (!file) { - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - vscode.window.showErrorMessage(vscode.l10n.t("Organize Imports failed. No resource provided.")); - return; - } - - const resource = activeEditor.document.uri; - const document = await vscode.workspace.openTextDocument(resource); - const openedFiledPath = this.client.toOpenTsFilePath(document); - if (!openedFiledPath) { - vscode.window.showErrorMessage(vscode.l10n.t("Organize Imports failed. Unknown file type.")); - return; - } - - file = openedFiledPath; - } - - const args: Proto.OrganizeImportsRequestArgs = { - scope: { - type: 'file', - args: { - file - } - }, - // Deprecated in 4.9; `mode` takes priority - skipDestructiveCodeActions: this.commandMetadata.mode === OrganizeImportsMode.SortAndCombine, - mode: typeConverters.OrganizeImportsMode.toProtocolOrganizeImportsMode(this.commandMetadata.mode), - }; - const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, nulToken)); - if (response.type !== 'response' || !response.body) { - return; - } - - if (response.body.length) { - const edits = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body); - return vscode.workspace.applyEdit(edits); - } } } -class ImportsCodeActionProvider implements vscode.CodeActionProvider { +class ImportCodeAction extends vscode.CodeAction { + constructor( + title: string, + kind: vscode.CodeActionKind, + public readonly document: vscode.TextDocument, + ) { + super(title, kind); + } +} + +class ImportsCodeActionProvider implements vscode.CodeActionProvider { constructor( private readonly client: ITypeScriptServiceClient, @@ -117,31 +89,62 @@ class ImportsCodeActionProvider implements vscode.CodeActionProvider { private readonly fileConfigManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, ) { - for (const id of commandMetadata.ids) { - commandManager.register(new OrganizeImportsCommand(id, commandMetadata, client, telemetryReporter)); - } + commandManager.register(new DidOrganizeImportsCommand(telemetryReporter)); } public provideCodeActions( document: vscode.TextDocument, _range: vscode.Range, context: vscode.CodeActionContext, - token: vscode.CancellationToken - ): vscode.CodeAction[] { + _token: vscode.CancellationToken + ): ImportCodeAction[] { + if (!context.only?.contains(this.commandMetadata.kind)) { + return []; + } + const file = this.client.toOpenTsFilePath(document); if (!file) { return []; } - if (!context.only?.contains(this.commandMetadata.kind)) { - return []; + return [new ImportCodeAction(this.commandMetadata.title, this.commandMetadata.kind, document)]; + } + + async resolveCodeAction(codeAction: ImportCodeAction, token: vscode.CancellationToken): Promise { + const response = await this.client.interruptGetErr(async () => { + await this.fileConfigManager.ensureConfigurationForDocument(codeAction.document, token); + if (token.isCancellationRequested) { + return; + } + + const file = this.client.toOpenTsFilePath(codeAction.document); + if (!file) { + return; + } + + const args: Proto.OrganizeImportsRequestArgs = { + scope: { + type: 'file', + args: { file } + }, + // Deprecated in 4.9; `mode` takes priority + skipDestructiveCodeActions: this.commandMetadata.mode === OrganizeImportsMode.SortAndCombine, + mode: typeConverters.OrganizeImportsMode.toProtocolOrganizeImportsMode(this.commandMetadata.mode), + }; + + return this.client.execute('organizeImports', args, nulToken); + }); + if (response?.type !== 'response' || !response.body || token.isCancellationRequested) { + return; } - this.fileConfigManager.ensureConfigurationForDocument(document, token); + if (response.body.length) { + codeAction.edit = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body); + } - const action = new vscode.CodeAction(this.commandMetadata.title, this.commandMetadata.kind); - action.command = { title: '', command: this.commandMetadata.ids[0], arguments: [file] }; - return [action]; + codeAction.command = { command: DidOrganizeImportsCommand.ID, title: '', arguments: [] }; + + return codeAction; } } @@ -155,15 +158,29 @@ export function register( const disposables: vscode.Disposable[] = []; for (const command of [organizeImportsCommand, sortImportsCommand, removeUnusedImportsCommand]) { - disposables.push(conditionalRegistration([ - requireMinVersion(client, command.minVersion ?? API.defaultVersion), - requireSomeCapability(client, ClientCapability.Semantic), - ], () => { - const provider = new ImportsCodeActionProvider(client, command, commandManager, fileConfigurationManager, telemetryReporter); - return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, { - providedCodeActionKinds: [command.kind] - }); - })); + disposables.push( + conditionalRegistration([ + requireMinVersion(client, command.minVersion ?? API.defaultVersion), + requireSomeCapability(client, ClientCapability.Semantic), + ], () => { + const provider = new ImportsCodeActionProvider(client, command, commandManager, fileConfigurationManager, telemetryReporter); + return vscode.Disposable.from( + vscode.languages.registerCodeActionsProvider(selector.semantic, provider, { + providedCodeActionKinds: [command.kind] + })); + }), + // Always register these commands. We will show a warning if the user tries to run them on an unsupported version + ...command.commandIds.map(id => + commandManager.register({ + id, + execute() { + return vscode.commands.executeCommand('editor.action.sourceAction', { + kind: command.kind.value, + apply: 'first', + }); + } + })) + ); } return vscode.Disposable.from(...disposables); diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index a61758cc66d..0364b7fad3e 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -40,6 +40,7 @@ function toWorkspaceEdit(client: ITypeScriptServiceClient, edits: readonly Proto namespace DidApplyRefactoringCommand { export interface Args { readonly action: string; + readonly trigger: vscode.CodeActionTriggerKind; } } @@ -56,6 +57,7 @@ class DidApplyRefactoringCommand implements Command { "refactor.execute" : { "owner": "mjbvz", "action" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, + "trigger" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "${include}": [ "${TypeScriptCommonProperties}" ] @@ -63,6 +65,7 @@ class DidApplyRefactoringCommand implements Command { */ this.telemetryReporter.logTelemetry('refactor.execute', { action: args.action, + trigger: args.trigger, }); } } @@ -71,6 +74,7 @@ namespace SelectRefactorCommand { readonly document: vscode.TextDocument; readonly refactor: Proto.ApplicableRefactorInfo; readonly rangeOrSelection: vscode.Range | vscode.Selection; + readonly trigger: vscode.CodeActionTriggerKind; } } @@ -97,7 +101,7 @@ class SelectRefactorCommand implements Command { return; } - const tsAction = new InlinedCodeAction(this.client, args.document, args.refactor, selected.action, args.rangeOrSelection); + const tsAction = new InlinedCodeAction(this.client, args.document, args.refactor, selected.action, args.rangeOrSelection, args.trigger); await tsAction.resolve(nulToken); if (tsAction.edit) { @@ -118,6 +122,7 @@ namespace MoveToFileRefactorCommand { readonly document: vscode.TextDocument; readonly action: Proto.RefactorActionInfo; readonly range: vscode.Range; + readonly trigger: vscode.CodeActionTriggerKind; } } @@ -158,7 +163,7 @@ class MoveToFileRefactorCommand implements Command { return; } - await this.didApplyCommand.execute({ action: args.action.name }); + await this.didApplyCommand.execute({ action: args.action.name, trigger: args.trigger }); } private async getTargetFile(document: vscode.TextDocument, file: string, range: vscode.Range): Promise { @@ -347,6 +352,7 @@ class InlinedCodeAction extends vscode.CodeAction { public readonly refactor: Proto.ApplicableRefactorInfo, public readonly action: Proto.RefactorActionInfo, public readonly range: vscode.Range, + trigger: vscode.CodeActionTriggerKind, ) { const title = action.description; super(title, InlinedCodeAction.getKind(action)); @@ -358,7 +364,7 @@ class InlinedCodeAction extends vscode.CodeAction { this.command = { title, command: DidApplyRefactoringCommand.ID, - arguments: [{ action: action.name }], + arguments: [{ action: action.name, trigger } satisfies DidApplyRefactoringCommand.Args], }; } @@ -420,6 +426,7 @@ class MoveToFileCodeAction extends vscode.CodeAction { document: vscode.TextDocument, action: Proto.RefactorActionInfo, range: vscode.Range, + trigger: vscode.CodeActionTriggerKind, ) { super(action.description, Move_File.kind); @@ -430,7 +437,7 @@ class MoveToFileCodeAction extends vscode.CodeAction { this.command = { title: action.description, command: MoveToFileRefactorCommand.ID, - arguments: [{ action, document, range }] + arguments: [{ action, document, range, trigger } satisfies MoveToFileRefactorCommand.Args] }; } } @@ -439,13 +446,14 @@ class SelectCodeAction extends vscode.CodeAction { constructor( info: Proto.ApplicableRefactorInfo, document: vscode.TextDocument, - rangeOrSelection: vscode.Range | vscode.Selection + rangeOrSelection: vscode.Range | vscode.Selection, + trigger: vscode.CodeActionTriggerKind, ) { super(info.description, vscode.CodeActionKind.Refactor); this.command = { title: info.description, command: SelectRefactorCommand.ID, - arguments: [{ action: this, document, refactor: info, rangeOrSelection }] + arguments: [{ document, refactor: info, rangeOrSelection, trigger } satisfies SelectRefactorCommand.Args] }; } } @@ -495,7 +503,9 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { + const response = await this.interruptGetErrIfNeeded(context, () => { const file = this.client.toOpenTsFilePath(document); if (!file) { return undefined; } + this.formattingOptionsManager.ensureConfigurationForDocument(document, token); const args: Proto.GetApplicableRefactorsRequestArgs = { @@ -550,7 +561,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { if (this.client.apiVersion.lt(API.v430)) { // Don't show 'infer return type' refactoring unless it has been explicitly requested @@ -585,6 +596,17 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider(context: vscode.CodeActionContext, f: () => R): R { + // Only interrupt diagnostics computation when code actions are explicitly + // (such as using the refactor command or a keybinding). This is a clear + // user action so we want to return results as quickly as possible. + if (context.triggerKind === vscode.CodeActionTriggerKind.Invoke) { + return this.client.interruptGetErr(f); + } else { + return f(); + } + } + public async resolveCodeAction( codeAction: TsCodeAction, token: vscode.CancellationToken, @@ -601,15 +623,16 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { for (const refactor of refactors) { if (refactor.inlineable === false) { - yield new SelectCodeAction(refactor, document, rangeOrSelection); + yield new SelectCodeAction(refactor, document, rangeOrSelection, context.triggerKind); } else { for (const action of refactor.actions) { - for (const codeAction of this.refactorActionToCodeActions(document, refactor, action, rangeOrSelection, refactor.actions)) { + for (const codeAction of this.refactorActionToCodeActions(document, context, refactor, action, rangeOrSelection, refactor.actions)) { yield codeAction; } } @@ -619,6 +642,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider c.textChanges) ) : expand.range; + const initialSelection = initialRange ? new vscode.Selection(initialRange.start, initialRange.end) : undefined; await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, + initialSelection, message, autoSend: true, }); diff --git a/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts b/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts index f9cdbf79afb..3240c524781 100644 --- a/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts +++ b/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts @@ -12,6 +12,7 @@ import type * as Proto from '../tsServer/protocol/protocol'; import * as PConst from '../tsServer/protocol/protocol.const'; import * as typeConverters from '../typeConverters'; import { ITypeScriptServiceClient } from '../typescriptService'; +import { coalesce } from '../utils/arrays'; function getSymbolKind(item: Proto.NavtoItem): vscode.SymbolKind { switch (item.kind) { @@ -64,9 +65,7 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide return []; } - return response.body - .filter(item => item.containerName || item.kind !== 'alias') - .map(item => this.toSymbolInformation(item)); + return coalesce(response.body.map(item => this.toSymbolInformation(item))); } private get searchAllOpenProjects() { @@ -89,13 +88,22 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide return this.client.toOpenTsFilePath(document); } - private toSymbolInformation(item: Proto.NavtoItem) { + private toSymbolInformation(item: Proto.NavtoItem): vscode.SymbolInformation | undefined { + if (item.kind === 'alias' && !item.containerName) { + return; + } + + const uri = this.client.toResource(item.file); + if (fileSchemes.isOfScheme(uri, fileSchemes.chatCodeBlock, fileSchemes.chatBackingCodeBlock)) { + return; + } + const label = TypeScriptWorkspaceSymbolProvider.getLabel(item); const info = new vscode.SymbolInformation( label, getSymbolKind(item), item.containerName || '', - typeConverters.Location.fromTextSpan(this.client.toResource(item.file), item)); + typeConverters.Location.fromTextSpan(uri, item)); const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined; if (kindModifiers?.has(PConst.KindModifiers.deprecated)) { info.tags = [vscode.SymbolTag.Deprecated]; diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 6157c3b8cb4..7b95591604b 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -9,6 +9,7 @@ import { CommandManager } from './commands/commandManager'; import { DocumentSelector } from './configuration/documentSelector'; import * as fileSchemes from './configuration/fileSchemes'; import { LanguageDescription } from './configuration/languageDescription'; +import { Schemes } from './configuration/schemes'; import { DiagnosticKind } from './languageFeatures/diagnostics'; import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; import { TelemetryReporter } from './logging/telemetry'; @@ -17,7 +18,7 @@ import { ClientCapability } from './typescriptService'; import TypeScriptServiceClient from './typescriptServiceClient'; import TypingsStatus from './ui/typingsStatus'; import { Disposable } from './utils/dispose'; -import { isWeb } from './utils/platform'; +import { isWeb, isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; const validateSetting = 'validate.enable'; @@ -64,6 +65,7 @@ export default class LanguageProvider extends Disposable { import('./languageFeatures/codeLens/implementationsCodeLens').then(provider => this._register(provider.register(selector, this.description, this.client, cachedNavTreeResponse))), import('./languageFeatures/codeLens/referencesCodeLens').then(provider => this._register(provider.register(selector, this.description, this.client, cachedNavTreeResponse))), import('./languageFeatures/completions').then(provider => this._register(provider.register(selector, this.description, this.client, this.typingsStatus, this.fileConfigurationManager, this.commandManager, this.telemetryReporter, this.onCompletionAccepted))), + import('./languageFeatures/copyPaste').then(provider => this._register(provider.register(selector, this.description, this.client))), import('./languageFeatures/definitions').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/directiveCommentCompletions').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/documentHighlight').then(provider => this._register(provider.register(selector, this.client))), @@ -136,12 +138,28 @@ export default class LanguageProvider extends Disposable { this.client.bufferSyncSupport.requestAllDiagnostics(); } - public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[]): void { + public diagnosticsReceived( + diagnosticsKind: DiagnosticKind, + file: vscode.Uri, + diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[], + ranges: vscode.Range[] | undefined): void { if (diagnosticsKind !== DiagnosticKind.Syntax && !this.client.hasCapabilityForResource(file, ClientCapability.Semantic)) { return; } - if (diagnosticsKind === DiagnosticKind.Semantic && isWeb() && this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors) { + if (diagnosticsKind === DiagnosticKind.Semantic && isWeb()) { + if ( + !isWebAndHasSharedArrayBuffers() + || !supportsReadableByteStreams() // No ata. Will result in lots of false positives + || this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors + || !this.client.configuration.webProjectWideIntellisenseEnabled + ) { + return; + } + } + + // Disable semantic errors in notebooks until we have better notebook support + if (diagnosticsKind === DiagnosticKind.Semantic && file.scheme === Schemes.notebookCell) { return; } @@ -161,7 +179,7 @@ export default class LanguageProvider extends Disposable { } } return true; - })); + }), ranges); } public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void { diff --git a/extensions/typescript-language-features/src/task/taskProvider.ts b/extensions/typescript-language-features/src/task/taskProvider.ts index c1b14385603..3cf0e3328fa 100644 --- a/extensions/typescript-language-features/src/task/taskProvider.ts +++ b/extensions/typescript-language-features/src/task/taskProvider.ts @@ -53,7 +53,7 @@ class TscTaskProvider extends Disposable implements vscode.TaskProvider { public async provideTasks(token: vscode.CancellationToken): Promise { const folders = vscode.workspace.workspaceFolders; - if ((this.autoDetect === AutoDetect.off) || !folders || !folders.length) { + if ((this.autoDetect === AutoDetect.off) || !folders?.length) { return []; } diff --git a/extensions/typescript-language-features/src/tsServer/api.ts b/extensions/typescript-language-features/src/tsServer/api.ts index 92e4503ec85..4beb29d1b2b 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -35,7 +35,10 @@ export class API { public static readonly v500 = API.fromSimpleString('5.0.0'); public static readonly v510 = API.fromSimpleString('5.1.0'); public static readonly v520 = API.fromSimpleString('5.2.0'); + public static readonly v544 = API.fromSimpleString('5.4.4'); public static readonly v540 = API.fromSimpleString('5.4.0'); + public static readonly v550 = API.fromSimpleString('5.5.0'); + public static readonly v560 = API.fromSimpleString('5.6.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); @@ -79,4 +82,8 @@ export class API { public lt(other: API): boolean { return !this.gte(other); } + + public isYarnPnp(): boolean { + return this.fullVersionString.includes('-sdk'); + } } diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 9f5d76f5ac3..2bb4d39ef26 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -227,7 +227,7 @@ class SyncedBuffer { return tsRoot?.startsWith(inMemoryResourcePrefix) ? undefined : tsRoot; } - return resource.scheme === fileSchemes.officeScript || resource.scheme === fileSchemes.chatCodeBlock ? '/' : undefined; + return fileSchemes.isOfScheme(resource, fileSchemes.officeScript, fileSchemes.chatCodeBlock, fileSchemes.chatBackingCodeBlock) ? '/' : undefined; } public get resource(): vscode.Uri { @@ -275,12 +275,12 @@ class SyncedBufferMap extends ResourceMap { } class PendingDiagnostics extends ResourceMap { - public getOrderedFileSet(): ResourceMap { + public getOrderedFileSet(): ResourceMap { const orderedResources = Array.from(this.entries()) .sort((a, b) => a.value - b.value) .map(entry => entry.resource); - const map = new ResourceMap(this._normalizePath, this.config); + const map = new ResourceMap(this._normalizePath, this.config); for (const resource of orderedResources) { map.set(resource, undefined); } @@ -292,7 +292,7 @@ class GetErrRequest { public static executeGetErrRequest( client: ITypeScriptServiceClient, - files: ResourceMap, + files: ResourceMap, onDone: () => void ) { return new GetErrRequest(client, files, onDone); @@ -303,7 +303,7 @@ class GetErrRequest { private constructor( private readonly client: ITypeScriptServiceClient, - public readonly files: ResourceMap, + public readonly files: ResourceMap, onDone: () => void ) { if (!this.isErrorReportingEnabled()) { @@ -313,19 +313,39 @@ class GetErrRequest { } const supportsSyntaxGetErr = this.client.apiVersion.gte(API.v440); - const allFiles = coalesce(Array.from(files.entries()) - .filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic)) + const fileEntries = Array.from(files.entries()).filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic)); + const allFiles = coalesce(fileEntries .map(entry => client.toTsFilePath(entry.resource))); if (!allFiles.length) { this._done = true; setImmediate(onDone); } else { - const request = this.areProjectDiagnosticsEnabled() + let request; + if (this.areProjectDiagnosticsEnabled()) { // Note that geterrForProject is almost certainly not the api we want here as it ends up computing far // too many diagnostics - ? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token) - : client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token); + request = client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token); + } + else { + let requestFiles; + if (this.areRegionDiagnosticsEnabled()) { + requestFiles = coalesce(fileEntries + .map(entry => { + const file = client.toTsFilePath(entry.resource); + const ranges = entry.value; + if (file && ranges) { + return typeConverters.Range.toFileRangesRequestArgs(file, ranges); + } + + return file; + })); + } + else { + requestFiles = allFiles; + } + request = client.executeAsync('geterr', { delay: 0, files: requestFiles }, this._token.token); + } request.finally(() => { if (this._done) { @@ -350,6 +370,10 @@ class GetErrRequest { return this.client.configuration.enableProjectDiagnostics && this.client.capabilities.has(ClientCapability.Semantic); } + private areRegionDiagnosticsEnabled() { + return this.client.configuration.enableRegionDiagnostics && this.client.apiVersion.gte(API.v560); + } + public cancel(): any { if (!this._done) { this._token.cancel(); @@ -638,6 +662,10 @@ export default class BufferSyncSupport extends Disposable { this.synchronizer.beforeCommand(command); } + public lineCount(resource: vscode.Uri): number | undefined { + return this.syncedBuffers.get(resource)?.lineCount; + } + private onDidCloseTextDocument(document: vscode.TextDocument): void { this.closeResource(document.uri); } @@ -722,7 +750,9 @@ export default class BufferSyncSupport extends Disposable { // Add all open TS buffers to the geterr request. They might be visible for (const buffer of this.syncedBuffers.values()) { - orderedFileSet.set(buffer.resource, undefined); + const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.toString() === buffer.resource.toString()); + const visibleRanges = editors.flatMap(editor => editor.visibleRanges); + orderedFileSet.set(buffer.resource, visibleRanges.length ? visibleRanges : undefined); } for (const { resource } of orderedFileSet.entries()) { @@ -752,7 +782,7 @@ export default class BufferSyncSupport extends Disposable { } private shouldValidate(buffer: SyncedBuffer): boolean { - if (buffer.resource.scheme === fileSchemes.chatCodeBlock) { + if (fileSchemes.isOfScheme(buffer.resource, fileSchemes.chatCodeBlock, fileSchemes.chatBackingCodeBlock)) { return false; } diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts index deb3357e6ae..ed4806e6fdd 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts @@ -78,6 +78,7 @@ export enum EventName { syntaxDiag = 'syntaxDiag', semanticDiag = 'semanticDiag', suggestionDiag = 'suggestionDiag', + regionSemanticDiag = 'regionSemanticDiag', configFileDiag = 'configFileDiag', telemetry = 'telemetry', projectLanguageServiceState = 'projectLanguageServiceState', @@ -88,6 +89,10 @@ export enum EventName { surveyReady = 'surveyReady', projectLoadingStart = 'projectLoadingStart', projectLoadingFinish = 'projectLoadingFinish', + createFileWatcher = 'createFileWatcher', + createDirectoryWatcher = 'createDirectoryWatcher', + closeFileWatcher = 'closeFileWatcher', + requestCompleted = 'requestCompleted', } export enum OrganizeImportsMode { diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index 45e09d63481..747e7c22e37 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -19,43 +19,5 @@ declare module '../../../../node_modules/typescript/lib/typescript' { interface Response { readonly _serverType?: ServerType; } - - export interface MapCodeRequestArgs { - /// The files and changes to try and apply/map. - mappings: MapCodeRequestDocumentMapping[]; - - /// Edits to apply to the current workspace before performing the mapping. - updates?: FileCodeEdits[] - } - - export interface MapCodeRequestDocumentMapping { - /// The file for the request (absolute pathname required). Null/undefined - /// if specific file is unknown. - file?: string; - - /// Optional name of project that contains file - projectFileName?: string; - - /// The specific code to map/insert/replace in the file. - contents: string[]; - - /// Areas of "focus" to inform the code mapper with. For example, cursor - /// location, current selection, viewport, etc. Nested arrays denote - /// priority: toplevel arrays are more important than inner arrays, and - /// inner array priorities are based on items within that array. Items - /// earlier in the arrays have higher priority. - focusLocations?: FileSpan[][]; - } - - export interface MapCodeRequest extends Request { - command: 'mapCode', - arguments: MapCodeRequestArgs; - } - - export interface MapCodeResponse extends Response { - body: FileCodeEdits[] - } } } - - diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 883aa6830bd..095295030b5 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -166,6 +166,10 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { this._tracer.traceRequestCompleted(this._serverId, 'requestCompleted', seq, callback); callback.onSuccess(undefined); } + // @ts-expect-error until ts 5.6 + if ((event as Proto.RequestCompletedEvent).body.performanceData) { + this._onEvent.fire(event); + } } else { this._tracer.traceEvent(this._serverId, event); this._onEvent.fire(event); diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index bb57c2644b4..5adf1866112 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -8,12 +8,13 @@ import { ApiService, Requests } from '@vscode/sync-api-service'; import * as vscode from 'vscode'; import { TypeScriptServiceConfiguration } from '../configuration/configuration'; import { Logger } from '../logging/logger'; +import { supportsReadableByteStreams } from '../utils/platform'; import { FileWatcherManager } from './fileWatchingManager'; +import { NodeVersionManager } from './nodeManager'; import type * as Proto from './protocol/protocol'; import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server'; import { TypeScriptVersionManager } from './versionManager'; import { TypeScriptVersion } from './versionProvider'; -import { NodeVersionManager } from './nodeManager'; type BrowserWatchEvent = { type: 'watchDirectory' | 'watchFile'; @@ -39,7 +40,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { version: TypeScriptVersion, args: readonly string[], kind: TsServerProcessKind, - _configuration: TypeScriptServiceConfiguration, + configuration: TypeScriptServiceConfiguration, _versionManager: TypeScriptVersionManager, _nodeVersionManager: NodeVersionManager, tsServerLog: TsServerLog | undefined, @@ -49,10 +50,10 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { ...args, // Explicitly give TS Server its path so it can load local resources '--executingFilePath', tsServerPath, + // Enable/disable web type acquisition + (configuration.webTypeAcquisitionEnabled && supportsReadableByteStreams() ? '--experimentalTypeAcquisition' : '--disableAutomaticTypingAcquisition'), ]; - if (_configuration.webExperimentalTypeAcquisition) { - launchArgs.push('--experimentalTypeAcquisition'); - } + return new WorkerServerProcess(kind, tsServerPath, this._extensionUri, launchArgs, tsServerLog, this._logger); } } diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index d03c7ea5df0..7dbde90f792 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -278,8 +278,7 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { } const childProcess = execPath ? - child_process.spawn(JSON.stringify(execPath), [...execArgv, tsServerPath, ...runtimeArgs], { - shell: true, + child_process.spawn(execPath, [...execArgv, tsServerPath, ...runtimeArgs], { windowsHide: true, cwd: undefined, env, diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 52dcf5baa19..364c0f07dae 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -242,7 +242,7 @@ export class TypeScriptServerSpawner { if (configuration.enableTsServerTracing && !isWeb()) { tsServerTraceDirectory = this._logDirectoryProvider.getNewLogDirectory(); if (tsServerTraceDirectory) { - args.push('--traceDirectory', tsServerTraceDirectory.fsPath); + args.push('--traceDirectory', `"${tsServerTraceDirectory.fsPath}"`); } } @@ -271,6 +271,14 @@ export class TypeScriptServerSpawner { args.push('--noGetErrOnBackgroundUpdate'); + if ( + apiVersion.gte(API.v544) + && configuration.useVsCodeWatcher + && !apiVersion.isYarnPnp() // Disable for yarn pnp as it currently breaks with the VS Code watcher + ) { + args.push('--canUseWatchEvents'); + } + args.push('--validateDefaultNpmLocation'); if (isWebAndHasSharedArrayBuffers()) { diff --git a/extensions/typescript-language-features/src/typeConverters.ts b/extensions/typescript-language-features/src/typeConverters.ts index 58babe2bda3..067a1ff3c0a 100644 --- a/extensions/typescript-language-features/src/typeConverters.ts +++ b/extensions/typescript-language-features/src/typeConverters.ts @@ -26,14 +26,24 @@ export namespace Range { Math.max(0, start.line - 1), Math.max(start.offset - 1, 0), Math.max(0, end.line - 1), Math.max(0, end.offset - 1)); - export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ - file, + // @ts-expect-error until ts 5.6 + export const toFileRange = (range: vscode.Range): Proto.FileRange => ({ startLine: range.start.line + 1, startOffset: range.start.character + 1, endLine: range.end.line + 1, endOffset: range.end.character + 1 }); + export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ + file, + ...toFileRange(range) + }); + // @ts-expect-error until ts 5.6 + export const toFileRangesRequestArgs = (file: string, ranges: vscode.Range[]): Proto.FileRangesRequestArgs => ({ + file, + ranges: ranges.map(toFileRange) + }); + export const toFormattingRequestArgs = (file: string, range: vscode.Range): Proto.FormatRequestArgs => ({ file, line: range.start.line + 1, diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index 7009799430e..c44bfc3ac3f 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -90,8 +90,8 @@ export default class TypeScriptServiceClientHost extends Disposable { services, allModeIds)); - this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => { - this.diagnosticsReceived(kind, resource, diagnostics); + this.client.onDiagnosticsReceived(({ kind, resource, diagnostics, spans }) => { + this.diagnosticsReceived(kind, resource, diagnostics, spans); }, null, this._disposables); this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables); @@ -236,14 +236,16 @@ export default class TypeScriptServiceClientHost extends Disposable { private async diagnosticsReceived( kind: DiagnosticKind, resource: vscode.Uri, - diagnostics: Proto.Diagnostic[] + diagnostics: Proto.Diagnostic[], + spans: Proto.TextSpan[] | undefined, ): Promise { const language = await this.findLanguage(resource); if (language) { language.diagnosticsReceived( kind, resource, - this.createMarkerDatas(diagnostics, language.diagnosticSource)); + this.createMarkerDatas(diagnostics, language.diagnosticSource), + spans?.map(span => typeConverters.Range.fromTextSpan(span))); } } @@ -255,13 +257,9 @@ export default class TypeScriptServiceClientHost extends Disposable { } this.findLanguage(this.client.toResource(body.configFile)).then(language => { - if (!language) { - return; - } - - language.configFileDiagnosticsReceived(this.client.toResource(body.configFile), body.diagnostics.map(tsDiag => { + language?.configFileDiagnosticsReceived(this.client.toResource(body.configFile), body.diagnostics.map(tsDiag => { const range = tsDiag.start && tsDiag.end ? typeConverters.Range.fromTextSpan(tsDiag) : new vscode.Range(0, 0, 0, 1); - const diagnostic = new vscode.Diagnostic(range, body.diagnostics[0].text, this.getDiagnosticSeverity(tsDiag)); + const diagnostic = new vscode.Diagnostic(range, tsDiag.text, this.getDiagnosticSeverity(tsDiag)); diagnostic.source = language.diagnosticSource; return diagnostic; })); diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 5823430c416..931b287df03 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -77,6 +77,7 @@ interface StandardTsServerRequests { 'getMoveToRefactoringFileSuggestions': [Proto.GetMoveToRefactoringFileSuggestionsRequestArgs, Proto.GetMoveToRefactoringFileSuggestions]; 'linkedEditingRange': [Proto.FileLocationRequestArgs, Proto.LinkedEditingRangeResponse]; 'mapCode': [Proto.MapCodeRequestArgs, Proto.MapCodeResponse]; + 'getPasteEdits': [Proto.GetPasteEditsRequestArgs, Proto.GetPasteEditsResponse]; } interface NoResponseTsServerRequests { @@ -86,6 +87,7 @@ interface NoResponseTsServerRequests { 'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null]; 'reloadProjects': [null, null]; 'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse]; + 'watchChange': [Proto.Request, null]; } interface AsyncTsServerRequests { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 812a9c457bf..7615436bd5d 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -5,38 +5,39 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration'; +import * as fileSchemes from './configuration/fileSchemes'; +import { Schemes } from './configuration/schemes'; import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter'; import { DiagnosticKind, DiagnosticsManager } from './languageFeatures/diagnostics'; -import * as Proto from './tsServer/protocol/protocol'; -import { EventName } from './tsServer/protocol/protocol.const'; +import { Logger } from './logging/logger'; +import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; +import Tracer from './logging/tracer'; +import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; import { API } from './tsServer/api'; import BufferSyncSupport from './tsServer/bufferSyncSupport'; import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { NodeVersionManager } from './tsServer/nodeManager'; import { TypeScriptPluginPathsProvider } from './tsServer/pluginPathsProvider'; +import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; +import * as Proto from './tsServer/protocol/protocol'; +import { EventName } from './tsServer/protocol/protocol.const'; import { ITypeScriptServer, TsServerLog, TsServerProcessFactory, TypeScriptServerExitEvent } from './tsServer/server'; import { TypeScriptServerError } from './tsServer/serverError'; import { TypeScriptServerSpawner } from './tsServer/spawner'; import { TypeScriptVersionManager } from './tsServer/versionManager'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider'; import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; -import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration'; -import { Disposable } from './utils/dispose'; -import * as fileSchemes from './configuration/fileSchemes'; -import { Logger } from './logging/logger'; +import { Disposable, DisposableStore, disposeAll } from './utils/dispose'; import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform'; -import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; -import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; -import Tracer from './logging/tracer'; -import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; -import { Schemes } from './configuration/schemes'; -import { NodeVersionManager } from './tsServer/nodeManager'; export interface TsDiagnostics { readonly kind: DiagnosticKind; readonly resource: vscode.Uri; readonly diagnostics: Proto.Diagnostic[]; + readonly spans?: Proto.TextSpan[]; } interface ToCancelOnResourceChanged { @@ -97,6 +98,12 @@ export const emptyAuthority = 'ts-nul-authority'; export const inMemoryResourcePrefix = '^'; +interface WatchEvent { + updated?: Set; + created?: Set; + deleted?: Set; +} + export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient { @@ -128,9 +135,13 @@ export default class TypeScriptServiceClient extends Disposable implements IType private readonly versionProvider: ITypeScriptVersionProvider; private readonly processFactory: TsServerProcessFactory; + private readonly watches = new Map(); + private readonly watchEvents = new Map(); + private watchChangeTimeout: NodeJS.Timeout | undefined; + constructor( private readonly context: vscode.ExtensionContext, - onCaseInsenitiveFileSystem: boolean, + onCaseInsensitiveFileSystem: boolean, services: { pluginManager: PluginManager; logDirectoryProvider: ILogDirectoryProvider; @@ -180,7 +191,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.restartTsServer(); })); - this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds, onCaseInsenitiveFileSystem); + this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds, onCaseInsensitiveFileSystem); this.onReady(() => { this.bufferSyncSupport.listen(); }); this.bufferSyncSupport.onDelete(resource => { @@ -221,7 +232,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType return this.apiVersion.fullVersionString; }); - this.diagnosticsManager = new DiagnosticsManager('typescript', this._configuration, this.telemetryReporter, onCaseInsenitiveFileSystem); + this.diagnosticsManager = new DiagnosticsManager('typescript', this._configuration, this.telemetryReporter, onCaseInsensitiveFileSystem); this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this._nodeVersionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory); this._register(this.pluginManager.onDidUpdateConfig(update => { @@ -298,6 +309,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType } this.loadingIndicator.reset(); + + this.resetWatchers(); } public restartTsServer(fromUserAction = false): void { @@ -401,6 +414,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.info(`Using Node installation from ${nodePath} to run TS Server`); } + this.resetWatchers(); + const apiVersion = version.apiVersion || API.defaultVersion; const mytoken = ++this.token; const handle = this.typescriptServerSpawner.spawn(version, this.capabilities, this.configuration, this.pluginManager, this.cancellerFactory, { @@ -409,6 +424,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.serverState = new ServerState.Running(handle, apiVersion, undefined, true); this.lastStart = Date.now(); + const hasGlobalPlugins = this.pluginManager.plugins.length > 0; /* __GDPR__ "tsserver.spawned" : { "owner": "mjbvz", @@ -416,12 +432,14 @@ export default class TypeScriptServiceClient extends Disposable implements IType "${TypeScriptCommonProperties}" ], "localTypeScriptVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "typeScriptVersionSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "typeScriptVersionSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasGlobalPlugins": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ this.logTelemetry('tsserver.spawned', { localTypeScriptVersion: this.versionProvider.localVersion ? this.versionProvider.localVersion.displayName : '', typeScriptVersionSource: version.source, + hasGlobalPlugins, }); handle.onError((err: Error) => { @@ -445,11 +463,12 @@ export default class TypeScriptServiceClient extends Disposable implements IType "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}" - ] + ], + "hasGlobalPlugins": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ - this.logTelemetry('tsserver.error'); - this.serviceExited(false); + this.logTelemetry('tsserver.error', { hasGlobalPlugins }); + this.serviceExited(false, apiVersion); }); handle.onExit((data: TypeScriptServerExitEvent) => { @@ -461,15 +480,19 @@ export default class TypeScriptServiceClient extends Disposable implements IType /* __GDPR__ "tsserver.exitWithCode" : { "owner": "mjbvz", - "code" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "signal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "${include}": [ "${TypeScriptCommonProperties}" - ] + ], + "code" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "signal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "hasGlobalPlugins": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ - this.logTelemetry('tsserver.exitWithCode', { code: code ?? undefined, signal: signal ?? undefined }); - + this.logTelemetry('tsserver.exitWithCode', { + code: code ?? undefined, + signal: signal ?? undefined, + hasGlobalPlugins, + }); if (this.token !== mytoken) { // this is coming from an old process @@ -479,7 +502,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (handle.tsServerLog?.type === 'file') { this.info(`TSServer log file: ${handle.tsServerLog.uri.fsPath}`); } - this.serviceExited(!this.isRestarting); + this.serviceExited(!this.isRestarting, apiVersion); this.isRestarting = false; }); @@ -493,6 +516,11 @@ export default class TypeScriptServiceClient extends Disposable implements IType return this.serverState; } + private resetWatchers() { + clearTimeout(this.watchChangeTimeout); + disposeAll(Array.from(this.watches.values())); + } + public async showVersionPicker(): Promise { this._versionManager.promptUserForVersion(); } @@ -593,7 +621,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType }; } - private serviceExited(restart: boolean): void { + private serviceExited(restart: boolean, tsVersion: API): void { + this.resetWatchers(); this.loadingIndicator.reset(); const previousState = this.serverState; @@ -653,7 +682,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (!this._isPromptingAfterCrash) { if (this.pluginManager.plugins.length) { prompt = vscode.window.showWarningMessage( - vscode.l10n.t("The JS/TS language service crashed.\nThis may be caused by a plugin contributed by one of these extensions: {0}.\nPlease try disabling these extensions before filing an issue against VS Code.", pluginExtensionList), reportIssueItem); + vscode.l10n.t("The JS/TS language service crashed.\nThis may be caused by a plugin contributed by one of these extensions: {0}.\nPlease try disabling these extensions before filing an issue against VS Code.", pluginExtensionList)); } else { prompt = vscode.window.showWarningMessage( vscode.l10n.t("The JS/TS language service crashed."), @@ -666,17 +695,34 @@ export default class TypeScriptServiceClient extends Disposable implements IType this._isPromptingAfterCrash = true; } - prompt?.then(item => { + prompt?.then(async item => { this._isPromptingAfterCrash = false; if (item === reportIssueItem) { + const minModernTsVersion = this.versionProvider.bundledVersion.apiVersion; - if ( + // Don't allow reporting issues using the PnP patched version of TS Server + if (tsVersion.isYarnPnp()) { + const reportIssue: vscode.MessageItem = { + title: vscode.l10n.t("Report issue against Yarn PnP"), + }; + const response = await vscode.window.showWarningMessage( + vscode.l10n.t("Please report an issue against Yarn PnP"), + { + modal: true, + detail: vscode.l10n.t("The workspace is using a version of the TypeScript Server that has been patched by Yarn PnP. This patching is a common source of bugs."), + }, + reportIssue); + + if (response === reportIssue) { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/yarnpkg/berry/issues')); + } + } + // Don't allow reporting issues with old TS versions + else if ( minModernTsVersion && - previousState.type === ServerState.Type.Errored && - previousState.error instanceof TypeScriptServerError && - previousState.error.version.apiVersion?.lt(minModernTsVersion) + tsVersion.lt(minModernTsVersion) ) { vscode.window.showWarningMessage( vscode.l10n.t("Please update your TypeScript version"), @@ -684,7 +730,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType modal: true, detail: vscode.l10n.t( "The workspace is using an old version of TypeScript ({0}).\n\nBefore reporting an issue, please update the workspace to use TypeScript {1} or newer to make sure the bug has not already been fixed.", - previousState.error.version.apiVersion.displayName, + tsVersion.displayName, minModernTsVersion.displayName), }); } else { @@ -911,7 +957,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType switch (event.event) { case EventName.syntaxDiag: case EventName.semanticDiag: - case EventName.suggestionDiag: { + case EventName.suggestionDiag: + case EventName.regionSemanticDiag: { // This event also roughly signals that projects have been loaded successfully (since the TS server is synchronous) this.loadingIndicator.reset(); @@ -920,19 +967,21 @@ export default class TypeScriptServiceClient extends Disposable implements IType this._onDiagnosticsReceived.fire({ kind: getDiagnosticsKind(event), resource: this.toResource(diagnosticEvent.body.file), - diagnostics: diagnosticEvent.body.diagnostics + diagnostics: diagnosticEvent.body.diagnostics, + // @ts-expect-error until ts 5.6 + spans: diagnosticEvent.body.spans, }); } - break; + return; } case EventName.configFileDiag: this._onConfigDiagnosticsReceived.fire(event as Proto.ConfigFileDiagnosticEvent); - break; + return; case EventName.telemetry: { const body = (event as Proto.TelemetryEvent).body; this.dispatchTelemetryEvent(body); - break; + return; } case EventName.projectLanguageServiceState: { const body = (event as Proto.ProjectLanguageServiceStateEvent).body!; @@ -940,7 +989,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.serverState.updateLanguageServiceEnabled(body.languageServiceEnabled); } this._onProjectLanguageServiceStateChanged.fire(body); - break; + return; } case EventName.projectsUpdatedInBackground: { this.loadingIndicator.reset(); @@ -948,32 +997,170 @@ export default class TypeScriptServiceClient extends Disposable implements IType const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body; const resources = body.openFiles.map(file => this.toResource(file)); this.bufferSyncSupport.getErr(resources); - break; + return; } case EventName.beginInstallTypes: this._onDidBeginInstallTypings.fire((event as Proto.BeginInstallTypesEvent).body); - break; + return; case EventName.endInstallTypes: this._onDidEndInstallTypings.fire((event as Proto.EndInstallTypesEvent).body); - break; + return; case EventName.typesInstallerInitializationFailed: this._onTypesInstallerInitializationFailed.fire((event as Proto.TypesInstallerInitializationFailedEvent).body); - break; + return; case EventName.surveyReady: this._onSurveyReady.fire((event as Proto.SurveyReadyEvent).body); - break; + return; case EventName.projectLoadingStart: this.loadingIndicator.startedLoadingProject((event as Proto.ProjectLoadingStartEvent).body.projectName); - break; + return; case EventName.projectLoadingFinish: this.loadingIndicator.finishedLoadingProject((event as Proto.ProjectLoadingFinishEvent).body.projectName); + return; + + case EventName.createDirectoryWatcher: { + const path = (event.body as Proto.CreateDirectoryWatcherEventBody).path; + if (path.startsWith(inMemoryResourcePrefix)) { + return; + } + + this.createFileSystemWatcher( + (event.body as Proto.CreateDirectoryWatcherEventBody).id, + new vscode.RelativePattern( + vscode.Uri.file(path), + (event.body as Proto.CreateDirectoryWatcherEventBody).recursive ? '**' : '*' + ), + (event.body as Proto.CreateDirectoryWatcherEventBody).ignoreUpdate + ); + return; + } + case EventName.createFileWatcher: { + const path = (event.body as Proto.CreateFileWatcherEventBody).path; + if (path.startsWith(inMemoryResourcePrefix)) { + return; + } + + this.createFileSystemWatcher( + (event.body as Proto.CreateFileWatcherEventBody).id, + new vscode.RelativePattern( + vscode.Uri.file(path), + '*' + ) + ); + return; + } + case EventName.closeFileWatcher: + this.closeFileSystemWatcher(event.body.id); + return; + + case EventName.requestCompleted: { + // @ts-expect-error until ts 5.6 + const diagnosticsDuration = (event.body as Proto.RequestCompletedEventBody).performanceData?.diagnosticsDuration; + if (diagnosticsDuration) { + this.diagnosticsManager.logDiagnosticsPerformanceTelemetry( + // @ts-expect-error until ts 5.6 + diagnosticsDuration.map(fileData => { + const resource = this.toResource(fileData.file); + return { + ...fileData, + lineCount: this.bufferSyncSupport.lineCount(resource), + }; + }) + ); + } + return; + } + } + } + + private scheduleExecuteWatchChangeRequest() { + if (!this.watchChangeTimeout) { + this.watchChangeTimeout = setTimeout(() => { + this.watchChangeTimeout = undefined; + const allEvents = Array.from(this.watchEvents, ([id, event]) => ({ + id, + updated: event.updated && Array.from(event.updated), + created: event.created && Array.from(event.created), + deleted: event.deleted && Array.from(event.deleted) + })); + this.watchEvents.clear(); + this.executeWithoutWaitingForResponse('watchChange', allEvents); + }, 100); /* aggregate events over 100ms to reduce client<->server IPC overhead */ + } + } + + private addWatchEvent(id: number, eventType: keyof WatchEvent, path: string) { + let event = this.watchEvents.get(id); + const removeEvent = (typeOfEventToRemove: keyof WatchEvent) => { + if (event?.[typeOfEventToRemove]?.delete(path) && event[typeOfEventToRemove].size === 0) { + event[typeOfEventToRemove] = undefined; + } + }; + const aggregateEvent = () => { + if (!event) { + this.watchEvents.set(id, event = {}); + } + (event[eventType] ??= new Set()).add(path); + }; + switch (eventType) { + case 'created': + removeEvent('deleted'); + removeEvent('updated'); + aggregateEvent(); + break; + case 'deleted': + removeEvent('created'); + removeEvent('updated'); + aggregateEvent(); + break; + case 'updated': + if (event?.created?.has(path)) { + return; + } + removeEvent('deleted'); + aggregateEvent(); break; } + this.scheduleExecuteWatchChangeRequest(); + } + + private createFileSystemWatcher( + id: number, + pattern: vscode.RelativePattern, + ignoreChangeEvents?: boolean, + ) { + const disposable = new DisposableStore(); + const watcher = disposable.add(vscode.workspace.createFileSystemWatcher(pattern, { excludes: [] /* TODO:: need to fill in excludes list */, ignoreChangeEvents })); + disposable.add(watcher.onDidChange(changeFile => + this.addWatchEvent(id, 'updated', changeFile.fsPath) + )); + disposable.add(watcher.onDidCreate(createFile => + this.addWatchEvent(id, 'created', createFile.fsPath) + )); + disposable.add(watcher.onDidDelete(deletedFile => + this.addWatchEvent(id, 'deleted', deletedFile.fsPath) + )); + disposable.add({ + dispose: () => { + this.watchEvents.delete(id); + this.watches.delete(id); + } + }); + + if (this.watches.has(id)) { + this.closeFileSystemWatcher(id); + } + this.watches.set(id, disposable); + } + + private closeFileSystemWatcher(id: number) { + const existing = this.watches.get(id); + existing?.dispose(); } private dispatchTelemetryEvent(telemetryData: Proto.TelemetryEventBody): void { @@ -1111,6 +1298,7 @@ function getDiagnosticsKind(event: Proto.Event) { case 'syntaxDiag': return DiagnosticKind.Syntax; case 'semanticDiag': return DiagnosticKind.Semantic; case 'suggestionDiag': return DiagnosticKind.Suggestion; + case 'regionSemanticDiag': return DiagnosticKind.RegionSemantic; } throw new Error('Unknown dignostics kind'); } diff --git a/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts b/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts index 3da1f5a8570..c3cae6321ff 100644 --- a/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts +++ b/extensions/typescript-language-features/src/ui/activeJsTsEditorTracker.ts @@ -28,6 +28,7 @@ export class ActiveJsTsEditorTracker extends Disposable { this._register(vscode.window.onDidChangeActiveTextEditor(_ => this.update())); this._register(vscode.window.onDidChangeVisibleTextEditors(_ => this.update())); + this._register(vscode.window.tabGroups.onDidChangeTabGroups(_ => this.update())); this.update(); } diff --git a/extensions/typescript-language-features/src/ui/intellisenseStatus.ts b/extensions/typescript-language-features/src/ui/intellisenseStatus.ts index 1a6ea63f427..e26e2b3719f 100644 --- a/extensions/typescript-language-features/src/ui/intellisenseStatus.ts +++ b/extensions/typescript-language-features/src/ui/intellisenseStatus.ts @@ -43,6 +43,8 @@ namespace IntellisenseState { export type State = typeof None | Pending | Resolved | typeof SyntaxOnly; } +type CreateOrOpenConfigCommandArgs = [root: vscode.Uri, projectType: ProjectType]; + export class IntellisenseStatus extends Disposable { public readonly openOpenConfigCommandId = '_typescript.openConfig'; @@ -62,7 +64,7 @@ export class IntellisenseStatus extends Disposable { commandManager.register({ id: this.openOpenConfigCommandId, - execute: async (root: vscode.Uri, projectType: ProjectType) => { + execute: async (...[root, projectType]: CreateOrOpenConfigCommandArgs) => { if (this._state.type === IntellisenseState.Type.Resolved) { await openProjectConfigOrPromptToCreate(projectType, this._client, root, this._state.configFile); } else if (this._state.type === IntellisenseState.Type.Pending) { @@ -72,7 +74,7 @@ export class IntellisenseStatus extends Disposable { }); commandManager.register({ id: this.createOrOpenConfigCommandId, - execute: async (root: vscode.Uri, projectType: ProjectType) => { + execute: async (...[root, projectType]: CreateOrOpenConfigCommandArgs) => { await openOrCreateConfig(this._client.apiVersion, projectType, root, this._client.configuration); }, }); @@ -182,7 +184,7 @@ export class IntellisenseStatus extends Disposable { title: this._state.projectType === ProjectType.TypeScript ? vscode.l10n.t("Configure tsconfig") : vscode.l10n.t("Configure jsconfig"), - arguments: [rootPath], + arguments: [rootPath, this._state.projectType] satisfies CreateOrOpenConfigCommandArgs, }; } else { statusItem.text = vscode.workspace.asRelativePath(this._state.configFile); @@ -190,7 +192,7 @@ export class IntellisenseStatus extends Disposable { statusItem.command = { command: this.openOpenConfigCommandId, title: vscode.l10n.t("Open config file"), - arguments: [rootPath], + arguments: [rootPath, this._state.projectType] satisfies CreateOrOpenConfigCommandArgs, }; } break; diff --git a/extensions/typescript-language-features/src/ui/managedFileContext.ts b/extensions/typescript-language-features/src/ui/managedFileContext.ts index 0b929f85277..1da4588a334 100644 --- a/extensions/typescript-language-features/src/ui/managedFileContext.ts +++ b/extensions/typescript-language-features/src/ui/managedFileContext.ts @@ -10,7 +10,7 @@ import { isSupportedLanguageMode } from '../configuration/languageIds'; import { Disposable } from '../utils/dispose'; import { ActiveJsTsEditorTracker } from './activeJsTsEditorTracker'; -/**E +/** * When clause context set when the current file is managed by vscode's built-in typescript extension. */ export default class ManagedFileContextManager extends Disposable { diff --git a/extensions/typescript-language-features/src/ui/typingsStatus.ts b/extensions/typescript-language-features/src/ui/typingsStatus.ts index 2e0d53b363e..3e8d7c4efac 100644 --- a/extensions/typescript-language-features/src/ui/typingsStatus.ts +++ b/extensions/typescript-language-features/src/ui/typingsStatus.ts @@ -11,7 +11,7 @@ import { Disposable } from '../utils/dispose'; const typingsInstallTimeout = 30 * 1000; export default class TypingsStatus extends Disposable { - private readonly _acquiringTypings = new Map(); + private readonly _acquiringTypings = new Map(); private readonly _client: ITypeScriptServiceClient; constructor(client: ITypeScriptServiceClient) { diff --git a/extensions/typescript-language-features/src/utils/arrays.ts b/extensions/typescript-language-features/src/utils/arrays.ts index a25bbe54733..61850a3de3e 100644 --- a/extensions/typescript-language-features/src/utils/arrays.ts +++ b/extensions/typescript-language-features/src/utils/arrays.ts @@ -20,5 +20,5 @@ export function equals( } export function coalesce(array: ReadonlyArray): T[] { - return array.filter(e => !!e); + return array.filter((e): e is T => !!e); } diff --git a/extensions/typescript-language-features/src/utils/async.ts b/extensions/typescript-language-features/src/utils/async.ts index db92754fd2e..9523d7fe67a 100644 --- a/extensions/typescript-language-features/src/utils/async.ts +++ b/extensions/typescript-language-features/src/utils/async.ts @@ -70,3 +70,94 @@ export function setImmediate(callback: (...args: any[]) => void, ...args: any[]) return { dispose: () => clearTimeout(handle) }; } } + + +/** + * A helper to prevent accumulation of sequential async tasks. + * + * Imagine a mail man with the sole task of delivering letters. As soon as + * a letter submitted for delivery, he drives to the destination, delivers it + * and returns to his base. Imagine that during the trip, N more letters were submitted. + * When the mail man returns, he picks those N letters and delivers them all in a + * single trip. Even though N+1 submissions occurred, only 2 deliveries were made. + * + * The throttler implements this via the queue() method, by providing it a task + * factory. Following the example: + * + * const throttler = new Throttler(); + * const letters = []; + * + * function deliver() { + * const lettersToDeliver = letters; + * letters = []; + * return makeTheTrip(lettersToDeliver); + * } + * + * function onLetterReceived(l) { + * letters.push(l); + * throttler.queue(deliver); + * } + */ +export class Throttler { + + private activePromise: Promise | null; + private queuedPromise: Promise | null; + private queuedPromiseFactory: ITask> | null; + + private isDisposed = false; + + constructor() { + this.activePromise = null; + this.queuedPromise = null; + this.queuedPromiseFactory = null; + } + + queue(promiseFactory: ITask>): Promise { + if (this.isDisposed) { + return Promise.reject(new Error('Throttler is disposed')); + } + + if (this.activePromise) { + this.queuedPromiseFactory = promiseFactory; + + if (!this.queuedPromise) { + const onComplete = () => { + this.queuedPromise = null; + + if (this.isDisposed) { + return; + } + + const result = this.queue(this.queuedPromiseFactory!); + this.queuedPromiseFactory = null; + + return result; + }; + + this.queuedPromise = new Promise(resolve => { + this.activePromise!.then(onComplete, onComplete).then(resolve); + }); + } + + return new Promise((resolve, reject) => { + this.queuedPromise!.then(resolve, reject); + }); + } + + this.activePromise = promiseFactory(); + + return new Promise((resolve, reject) => { + this.activePromise!.then((result: T) => { + this.activePromise = null; + resolve(result); + }, (err: unknown) => { + this.activePromise = null; + reject(err); + }); + }); + } + + dispose(): void { + this.isDisposed = true; + } +} diff --git a/extensions/typescript-language-features/src/utils/dispose.ts b/extensions/typescript-language-features/src/utils/dispose.ts index 7b6627204d5..a3730ee4540 100644 --- a/extensions/typescript-language-features/src/utils/dispose.ts +++ b/extensions/typescript-language-features/src/utils/dispose.ts @@ -6,10 +6,10 @@ import * as vscode from 'vscode'; export function disposeAll(disposables: vscode.Disposable[]) { - while (disposables.length) { - const item = disposables.pop(); - item?.dispose(); + for (const disposable of disposables) { + disposable.dispose(); } + disposables.length = 0; } export interface IDisposable { @@ -42,3 +42,12 @@ export abstract class Disposable { return this._isDisposed; } } + +export class DisposableStore extends Disposable { + + public add(disposable: T): T { + this._register(disposable); + + return disposable; + } +} diff --git a/extensions/typescript-language-features/src/utils/objects.ts b/extensions/typescript-language-features/src/utils/objects.ts index a31467bd8d6..88c5a435e1c 100644 --- a/extensions/typescript-language-features/src/utils/objects.ts +++ b/extensions/typescript-language-features/src/utils/objects.ts @@ -9,6 +9,7 @@ export function equals(one: any, other: any): boolean { if (one === other) { return true; } + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (one === null || one === undefined || other === null || other === undefined) { return false; } diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts index a7bdd8f30ff..ba954f59da3 100644 --- a/extensions/typescript-language-features/src/utils/platform.ts +++ b/extensions/typescript-language-features/src/utils/platform.ts @@ -12,3 +12,8 @@ export function isWeb(): boolean { export function isWebAndHasSharedArrayBuffers(): boolean { return isWeb() && (globalThis as any)['crossOriginIsolated']; } + +export function supportsReadableByteStreams(): boolean { + return isWeb() && 'ReadableByteStreamController' in globalThis; +} + diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index d40c03a591d..65557839ba6 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -11,10 +11,12 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts", "../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", + "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts", ] } diff --git a/extensions/typescript-language-features/web/src/fileWatcherManager.ts b/extensions/typescript-language-features/web/src/fileWatcherManager.ts index 8c8d7403740..5bbce244688 100644 --- a/extensions/typescript-language-features/web/src/fileWatcherManager.ts +++ b/extensions/typescript-language-features/web/src/fileWatcherManager.ts @@ -40,8 +40,6 @@ export class FileWatcherManager { return FileWatcherManager.noopWatcher; } - console.log('watching file:', path); - this.logger.logVerbose('fs.watchFile', { path }); let uri: URI; diff --git a/extensions/typescript-language-features/web/src/serverHost.ts b/extensions/typescript-language-features/web/src/serverHost.ts index f2f9ca95996..dedec85991f 100644 --- a/extensions/typescript-language-features/web/src/serverHost.ts +++ b/extensions/typescript-language-features/web/src/serverHost.ts @@ -11,6 +11,7 @@ import { FileWatcherManager } from './fileWatcherManager'; import { Logger } from './logging'; import { PathMapper, looksLikeNodeModules, mapUri } from './pathMapper'; import { findArgument, hasArgument } from './util/args'; +import { URI } from 'vscode-uri'; type ServerHostWithImport = ts.server.ServerHost & { importPlugin(root: string, moduleName: string): Promise }; @@ -338,13 +339,24 @@ function createServerHost( // For module resolution only. `node_modules` is also automatically mapped // as if all node_modules-like paths are symlinked. function realpath(path: string): string { + if (path.startsWith('/^/')) { + // In memory file. No mapping needed + return path; + } + const isNm = looksLikeNodeModules(path) && !path.startsWith('/vscode-global-typings/'); // skip paths without .. or ./ or /. And things that look like node_modules if (!isNm && !path.match(/\.\.|\/\.|\.\//)) { return path; } - let uri = pathMapper.toResource(path); + let uri: URI; + try { + uri = pathMapper.toResource(path); + } catch { + return path; + } + if (isNm) { uri = mapUri(uri, 'vscode-node-modules'); } diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index bc72fe4cb8b..e43e95500ce 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.17.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.11.tgz#c04054659d88bfeba94095f41ef99a8ddf4e1813" - integrity sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/semver@^5.5.0": version "5.5.0" @@ -164,6 +166,11 @@ tas-client@0.2.33: resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.2.33.tgz#451bf114a8a64748030ce4068ab7d079958402e6" integrity sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-tas-client@^0.1.84: version "0.1.84" resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 1c54b960a91..8a1032fef2c 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -7,18 +7,19 @@ "enabledApiProposals": [ "activeComment", "authSession", - "chatParticipant", - "languageModels", - "defaultChatParticipant", + "chatParticipantPrivate", + "chatProvider", "chatVariableResolver", - "contribViewsRemote", "contribStatusBarItems", + "contribViewsRemote", "createFileSystemWatcher", "customEditorMove", + "defaultChatParticipant", "diffCommand", "documentFiltersExclusive", "documentPaste", "editorInsets", + "embeddings", "extensionRuntime", "extensionsAny", "externalUriOpener", @@ -27,6 +28,8 @@ "findTextInFiles", "fsChunks", "interactive", + "languageStatusText", + "lmTools", "mappedEditsProvider", "notebookCellExecutionState", "notebookDeprecated", @@ -35,25 +38,23 @@ "notebookMime", "portsAttributes", "quickPickSortByLabel", - "languageStatusText", "resolvers", "scmActionButton", "scmSelectedProvider", "scmTextDocument", "scmValidation", "taskPresentationGroup", + "telemetry", "terminalDataWriteEvent", "terminalDimensions", - "tunnels", "testObserver", "textSearchProvider", "timeline", "tokenInformation", "treeViewActiveItem", "treeViewReveal", - "workspaceTrust", - "telemetry", - "windowActivity" + "tunnels", + "workspaceTrust" ], "private": true, "activationEvents": [], @@ -63,6 +64,11 @@ }, "icon": "media/icon.png", "contributes": { + "languageModels": [ + { + "vendor": "test-lm-vendor" + } + ], "chatParticipants": [ { "id": "api-test.participant", @@ -75,6 +81,12 @@ "description": "Hello" } ] + }, + { + "id": "api-test.participant2", + "name": "participant2", + "description": "test", + "commands": [] } ], "configuration": { @@ -99,6 +111,13 @@ "farboo.get": { "type": "string", "default": "get-prop" + }, + "integration-test.http.proxy": { + "type": "string" + }, + "integration-test.http.proxyAuth": { + "type": "string", + "default": "get-prop" } } }, @@ -236,7 +255,10 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x", + "@types/node-forge": "^1.3.11", + "node-forge": "^1.3.1", + "straightforward": "^4.2.2" }, "repository": { "type": "git", diff --git a/extensions/vscode-api-tests/src/memfs.ts b/extensions/vscode-api-tests/src/memfs.ts index b7392ae7195..cd422682499 100644 --- a/extensions/vscode-api-tests/src/memfs.ts +++ b/extensions/vscode-api-tests/src/memfs.ts @@ -218,7 +218,7 @@ export class TestFS implements vscode.FileSystemProvider { private _emitter = new vscode.EventEmitter(); private _bufferedEvents: vscode.FileChangeEvent[] = []; - private _fireSoonHandle?: NodeJS.Timer; + private _fireSoonHandle?: NodeJS.Timeout; readonly onDidChangeFile: vscode.Event = this._emitter.event; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index cd8614a86e7..ca72f39feb8 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import 'mocha'; -import { commands, CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; -import { DeferredPromise, assertNoRpc, closeAllEditors, disposeAll } from '../utils'; +import { ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, chat, commands } from 'vscode'; +import { DeferredPromise, asPromise, assertNoRpc, closeAllEditors, disposeAll } from '../utils'; suite('chat', () => { @@ -28,19 +28,12 @@ suite('chat', () => { return deferred; } - function setupParticipant(): Event<{ request: ChatRequest; context: ChatContext }> { + function setupParticipant(second?: boolean): Event<{ request: ChatRequest; context: ChatContext }> { const emitter = new EventEmitter<{ request: ChatRequest; context: ChatContext }>(); disposables.push(emitter); - disposables.push(interactive.registerInteractiveSessionProvider('provider', { - prepareSession: (_token: CancellationToken): ProviderResult => { - return { - requester: { name: 'test' }, - responder: { name: 'test' }, - }; - }, - })); - const participant = chat.createChatParticipant('api-test.participant', (request, context, _progress, _token) => { + const id = second ? 'api-test.participant2' : 'api-test.participant'; + const participant = chat.createChatParticipant(id, (request, context, _progress, _token) => { emitter.fire({ request, context }); }); participant.isDefault = true; @@ -52,23 +45,33 @@ suite('chat', () => { const onRequest = setupParticipant(); commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); + const deferred = new DeferredPromise(); let i = 0; disposables.push(onRequest(request => { - if (i === 0) { - assert.deepStrictEqual(request.request.command, 'hello'); - assert.strictEqual(request.request.prompt, 'friend'); - i++; - commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); - } else { - assert.strictEqual(request.context.history.length, 1); - assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); - assert.strictEqual(request.context.history[0].command, 'hello'); + try { + if (i === 0) { + assert.deepStrictEqual(request.request.command, 'hello'); + assert.strictEqual(request.request.prompt, 'friend'); + i++; + setTimeout(() => { + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); + }, 0); + } else { + assert.strictEqual(request.context.history.length, 2); + assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); + assert.strictEqual(request.context.history[0].command, 'hello'); + deferred.complete(); + } + } catch (e) { + deferred.error(e); } })); + + await deferred.p; }); test('participant and variable', async () => { - disposables.push(chat.registerChatVariableResolver('myVar', 'My variable', { + disposables.push(chat.registerChatVariableResolver('myVarId', 'myVar', 'My variable', 'My variable', false, { resolve(_name, _context, _token) { return [{ level: ChatVariableLevel.Full, value: 'myValue' }]; } @@ -78,19 +81,10 @@ suite('chat', () => { commands.executeCommand('workbench.action.chat.open', { query: '@participant hi #myVar' }); const request = await deferred.p; assert.strictEqual(request.prompt, 'hi #myVar'); - assert.strictEqual(request.variables[0].values[0].value, 'myValue'); + assert.strictEqual(request.references[0].value, 'myValue'); }); test('result metadata is returned to the followup provider', async () => { - disposables.push(interactive.registerInteractiveSessionProvider('provider', { - prepareSession: (_token: CancellationToken): ProviderResult => { - return { - requester: { name: 'test' }, - responder: { name: 'test' }, - }; - }, - })); - const deferred = new DeferredPromise(); const participant = chat.createChatParticipant('api-test.participant', (_request, _context, _progress, _token) => { return { metadata: { key: 'value' } }; @@ -108,4 +102,25 @@ suite('chat', () => { const result = await deferred.p; assert.deepStrictEqual(result.metadata, { key: 'value' }); }); + + test('isolated participant history', async () => { + const onRequest = setupParticipant(); + const onRequest2 = setupParticipant(true); + + commands.executeCommand('workbench.action.chat.open', { query: '@participant hi' }); + await asPromise(onRequest); + + // Request is still being handled at this point, wait for it to end + setTimeout(() => { + commands.executeCommand('workbench.action.chat.open', { query: '@participant2 hi' }); + }, 0); + const request2 = await asPromise(onRequest2); + assert.strictEqual(request2.context.history.length, 0); + + setTimeout(() => { + commands.executeCommand('workbench.action.chat.open', { query: '@participant2 hi' }); + }, 0); + const request3 = await asPromise(onRequest2); + assert.strictEqual(request3.context.history.length, 2); // request + response = 2 + }); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index ee33d61d1e0..cc2f2675297 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { basename } from 'path'; -import { commands, debug, Disposable, window, workspace } from 'vscode'; +import { commands, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode'; import { assertNoRpc, createRandomFile, disposeAll } from '../utils'; suite('vscode API - debug', function () { @@ -49,6 +49,17 @@ suite('vscode API - debug', function () { disposeAll(toDispose); }); + test('function breakpoint', async function () { + assert.strictEqual(debug.breakpoints.length, 0); + debug.addBreakpoints([new FunctionBreakpoint('func', false, 'condition', 'hitCondition', 'logMessage')]); + const functionBreakpoint = debug.breakpoints[0] as FunctionBreakpoint; + assert.strictEqual(functionBreakpoint.condition, 'condition'); + assert.strictEqual(functionBreakpoint.hitCondition, 'hitCondition'); + assert.strictEqual(functionBreakpoint.logMessage, 'logMessage'); + assert.strictEqual(functionBreakpoint.enabled, false); + assert.strictEqual(functionBreakpoint.functionName, 'func'); + }); + test('start debugging', async function () { let stoppedEvents = 0; let variablesReceived: () => void; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts index e2145d4ee28..b4212bb6103 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts @@ -37,7 +37,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed)); } } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -62,7 +62,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed + '\n')); } } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -88,7 +88,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem(`(${ranges.length})${selections.join(' ')}`)); } } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); editor.selections = [new vscode.Selection(0, 0, 0, 0)]; @@ -118,7 +118,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('a')); providerAResolve(); } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); // Later registered providers will be called first testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { @@ -132,7 +132,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('b')); } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -159,7 +159,7 @@ suite.skip('vscode API - Copy Paste', function () { dataTransfer.set(textPlain, new vscode.DataTransferItem('xyz')); providerAResolve(); } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { @@ -172,7 +172,7 @@ suite.skip('vscode API - Copy Paste', function () { const str = await entry!.asString(); dataTransfer.set(textPlain, new vscode.DataTransferItem(reverseString(str))); } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); @@ -192,13 +192,13 @@ suite.skip('vscode API - Copy Paste', function () { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { dataTransfer.set(textPlain, new vscode.DataTransferItem('xyz')); } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); testDisposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider { async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], _dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise { throw new Error('Expected testing error from bad provider'); } - }, { providedPasteEditKinds: [vscode.DocumentPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); + }, { providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Empty.append('test')], copyMimeTypes: [textPlain] })); await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); const newDocContent = getNextDocumentText(testDisposables, doc); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts index a3d4036e5a2..69810825b1b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts @@ -127,6 +127,9 @@ async function addCellAndRun(code: string, notebook: vscode.NotebookDocument) { }); function lastCellIsVisible(notebookEditor: vscode.NotebookEditor) { + if (!notebookEditor.visibleRanges.length) { + return false; + } const lastVisibleCell = notebookEditor.visibleRanges[notebookEditor.visibleRanges.length - 1].end; return notebookEditor.notebook.cellCount === lastVisibleCell; } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts new file mode 100644 index 00000000000..178119a1197 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils'; + + +suite('lm', function () { + + let disposables: vscode.Disposable[] = []; + + setup(function () { + disposables = []; + }); + + teardown(async function () { + assertNoRpc(); + await closeAllEditors(); + disposeAll(disposables); + }); + + + test('lm request and stream', async function () { + + let p: vscode.Progress | undefined; + const defer = new DeferredPromise(); + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, progress, _token) { + p = progress; + return defer.p; + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + const request = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + + // assert we have a request immediately + assert.ok(request); + assert.ok(p); + assert.strictEqual(defer.isSettled, false); + + let streamDone = false; + let responseText = ''; + + const pp = (async () => { + for await (const chunk of request.text) { + responseText += chunk; + } + streamDone = true; + })(); + + assert.strictEqual(responseText, ''); + assert.strictEqual(streamDone, false); + + p.report({ index: 0, part: 'Hello' }); + defer.complete(); + + await pp; + await new Promise(r => setTimeout(r, 1000)); + + assert.strictEqual(streamDone, true); + assert.strictEqual(responseText, 'Hello'); + }); + + test('lm request fail', async function () { + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + throw new Error('BAD'); + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + try { + await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(false, 'EXPECTED error'); + } catch (error) { + assert.ok(error instanceof Error); + } + }); + + test('lm stream fail', async function () { + + const defer = new DeferredPromise(); + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + return defer.p; + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + const res = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(res); + + const result = (async () => { + for await (const _chunk of res.text) { + + } + })(); + + defer.error(new Error('STREAM FAIL')); + + try { + await result; + assert.ok(false, 'EXPECTED error'); + } catch (error) { + assert.ok(error); + // assert.ok(error instanceof Error); // todo@jrieken requires one more insiders + } + }); +}); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts new file mode 100644 index 00000000000..ccda85b442a --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as https from 'https'; +import 'mocha'; +import { assertNoRpc, delay } from '../utils'; +import { pki } from 'node-forge'; +import { AddressInfo } from 'net'; +import { resetCaches } from '@vscode/proxy-agent'; +import * as vscode from 'vscode'; +import { middleware, Straightforward } from 'straightforward'; + +(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - network proxy support', () => { + + teardown(async function () { + assertNoRpc(); + }); + + test('custom root certificate', async () => { + const keys = pki.rsa.generateKeyPair(2048); + const cert = pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); + const attrs = [{ + name: 'commonName', + value: 'localhost-proxy-test' + }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.sign(keys.privateKey); + const certPEM = pki.certificateToPem(cert); + const privateKeyPEM = pki.privateKeyToPem(keys.privateKey); + + let resolvePort: (port: number) => void; + let rejectPort: (err: any) => void; + const port = new Promise((resolve, reject) => { + resolvePort = resolve; + rejectPort = reject; + }); + const server = https.createServer({ + key: privateKeyPEM, + cert: certPEM, + }, (_req, res) => { + res.end(); + }).listen(0, '127.0.0.1', () => { + const address = server.address(); + resolvePort((address as AddressInfo).port); + }).on('error', err => { + rejectPort(err); + }); + + // Using https.globalAgent because it is shared with proxyResolver.ts and mutable. + (https.globalAgent as any).testCertificates = [certPEM]; + resetCaches(); + + try { + const portNumber = await port; + await new Promise((resolve, reject) => { + https.get(`https://127.0.0.1:${portNumber}`, { servername: 'localhost-proxy-test' }, res => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Unexpected status code: ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + } finally { + delete (https.globalAgent as any).testCertificates; + resetCaches(); + server.close(); + } + }); + + test('basic auth', async () => { + const url = 'https://example.com'; // Need to use non-local URL because local URLs are excepted from proxying. + const user = 'testuser'; + const pass = 'testpassword'; + + const sf = new Straightforward(); + let authEnabled = false; + const auth = middleware.auth({ user, pass }); + sf.onConnect.use(async (context, next) => { + if (authEnabled) { + return auth(context, next); + } + next(); + }); + sf.onConnect.use(({ clientSocket }) => { + // Shortcircuit the request. + if (authEnabled) { + clientSocket.end('HTTP/1.1 204\r\n\r\n'); + } else { + clientSocket.end('HTTP/1.1 418\r\n\r\n'); + } + }); + const proxyListen = sf.listen(0); + + try { + await proxyListen; + const proxyPort = (sf.server.address() as AddressInfo).port; + + await vscode.workspace.getConfiguration().update('integration-test.http.proxy', `PROXY 127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); + await delay(1000); // Wait for the configuration change to propagate. + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 418) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 418): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + + authEnabled = true; + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 407) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 407): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + + await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', `${user}:${pass}`, vscode.ConfigurationTarget.Global); + await delay(1000); // Wait for the configuration change to propagate. + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 204) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 204): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + } finally { + sf.close(); + await vscode.workspace.getConfiguration().update('integration-test.http.proxy', undefined, vscode.ConfigurationTarget.Global); + await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', undefined, vscode.ConfigurationTarget.Global); + } + }); +}); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts index 64b6354ac9d..f10edc51b3b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts @@ -24,7 +24,7 @@ suite('vscode API - globalState / workspaceState', () => { let res = state.get('state.test.get', 'default'); assert.strictEqual(res, 'default'); - await state.update('state.test.get', 'testvalue'); + state.update('state.test.get', 'testvalue'); keys = state.keys(); assert.strictEqual(keys.length, 1); @@ -33,7 +33,7 @@ suite('vscode API - globalState / workspaceState', () => { res = state.get('state.test.get', 'default'); assert.strictEqual(res, 'testvalue'); - await state.update('state.test.get', undefined); + state.update('state.test.get', undefined); keys = state.keys(); assert.strictEqual(keys.length, 0, `Unexpected keys: ${JSON.stringify(keys)}`); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts new file mode 100644 index 00000000000..ac6287c2f35 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, notStrictEqual, ok, strictEqual } from 'assert'; +import { platform } from 'os'; +import { env, TerminalShellExecutionCommandLineConfidence, UIKind, window, workspace, type Disposable, type Terminal, type TerminalShellExecution, type TerminalShellExecutionCommandLine, type TerminalShellExecutionEndEvent, type TerminalShellIntegration } from 'vscode'; +import { assertNoRpc } from '../utils'; + +// Terminal integration tests are disabled on web https://github.com/microsoft/vscode/issues/92826 +// Windows images will often not have functional shell integration +// TODO: Linux https://github.com/microsoft/vscode/issues/221399 +(env.uiKind === UIKind.Web || platform() === 'win32' || platform() === 'linux' ? suite.skip : suite)('vscode API - Terminal.shellIntegration', () => { + const disposables: Disposable[] = []; + + suiteSetup(async () => { + const config = workspace.getConfiguration('terminal.integrated'); + await config.update('shellIntegration.enabled', true); + }); + + suiteTeardown(async () => { + const config = workspace.getConfiguration('terminal.integrated'); + await config.update('shellIntegration.enabled', undefined); + }); + + teardown(async () => { + assertNoRpc(); + disposables.forEach(d => d.dispose()); + disposables.length = 0; + }); + + function createTerminalAndWaitForShellIntegration(): Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }> { + return new Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }>(resolve => { + disposables.push(window.onDidChangeTerminalShellIntegration(e => { + if (e.terminal === terminal) { + resolve({ + terminal, + shellIntegration: e.shellIntegration + }); + } + })); + const terminal = platform() === 'win32' + ? window.createTerminal() + : window.createTerminal({ shellPath: '/bin/bash' }); + terminal.show(); + }); + } + + function executeCommandAsync(shellIntegration: TerminalShellIntegration, command: string, args?: string[]): { execution: Promise; endEvent: Promise } { + return { + execution: new Promise(resolve => { + // Await a short period as pwsh's first SI prompt can fail when launched in quick succession + setTimeout(() => { + if (args) { + resolve(shellIntegration.executeCommand(command, args)); + } else { + resolve(shellIntegration.executeCommand(command)); + } + }, 500); + }), + endEvent: new Promise(resolve => { + disposables.push(window.onDidEndTerminalShellExecution(e => { + if (e.shellIntegration === shellIntegration) { + resolve(e); + } + })); + }) + }; + } + + function closeTerminalAsync(terminal: Terminal): Promise { + return new Promise(resolve => { + disposables.push(window.onDidCloseTerminal(e => { + if (e === terminal) { + resolve(); + } + })); + terminal.dispose(); + }); + } + + test('window.onDidChangeTerminalShellIntegration should activate for the default terminal', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + ok(terminal.shellIntegration); + ok(shellIntegration); + await closeTerminalAsync(terminal); + }); + + test('execution events should fire in order when a command runs', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const events: string[] = []; + disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start'))); + disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end'))); + + await executeCommandAsync(shellIntegration, 'echo hello').endEvent; + + deepStrictEqual(events, ['start', 'end']); + + await closeTerminalAsync(terminal); + }); + + test('end execution event should report zero exit code for successful commands', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const events: string[] = []; + disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start'))); + disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end'))); + + const endEvent = await executeCommandAsync(shellIntegration, 'echo hello').endEvent; + strictEqual(endEvent.exitCode, 0); + + await closeTerminalAsync(terminal); + }); + + test('end execution event should report non-zero exit code for failed commands', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const events: string[] = []; + disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start'))); + disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end'))); + + const endEvent = await executeCommandAsync(shellIntegration, 'fakecommand').endEvent; + notStrictEqual(endEvent.exitCode, 0); + + await closeTerminalAsync(terminal); + }); + + test('TerminalShellExecution.read iterables should be available between the start and end execution events', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const events: string[] = []; + disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start'))); + disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end'))); + + const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello'); + for await (const _ of (await execution).read()) { + events.push('data'); + } + await endEvent; + + ok(events.length >= 3, `should have at least 3 events ${JSON.stringify(events)}`); + strictEqual(events[0], 'start', `first event should be 'start' ${JSON.stringify(events)}`); + strictEqual(events.at(-1), 'end', `last event should be 'end' ${JSON.stringify(events)}`); + for (let i = 1; i < events.length - 1; i++) { + strictEqual(events[i], 'data', `all middle events should be 'data' ${JSON.stringify(events)}`); + } + + await closeTerminalAsync(terminal); + }); + + test('TerminalShellExecution.read events should fire with contents of command', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const events: string[] = []; + + const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello'); + for await (const data of (await execution).read()) { + events.push(data); + } + await endEvent; + + ok(events.join('').includes('hello'), `should include 'hello' in ${JSON.stringify(events)}`); + + await closeTerminalAsync(terminal); + }); + + test('TerminalShellExecution.read events should give separate iterables per call', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + + const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello'); + const executionSync = await execution; + const firstRead = executionSync.read(); + const secondRead = executionSync.read(); + + const [firstReadEvents, secondReadEvents] = await Promise.all([ + new Promise(resolve => { + (async () => { + const events: string[] = []; + for await (const data of firstRead) { + events.push(data); + } + resolve(events); + })(); + }), + new Promise(resolve => { + (async () => { + const events: string[] = []; + for await (const data of secondRead) { + events.push(data); + } + resolve(events); + })(); + }), + ]); + await endEvent; + + ok(firstReadEvents.join('').includes('hello'), `should include 'hello' in ${JSON.stringify(firstReadEvents)}`); + deepStrictEqual(firstReadEvents, secondReadEvents); + + await closeTerminalAsync(terminal); + }); + + test('executeCommand(commandLine)', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello'); + const executionSync = await execution; + const expectedCommandLine: TerminalShellExecutionCommandLine = { + value: 'echo hello', + isTrusted: true, + confidence: TerminalShellExecutionCommandLineConfidence.High + }; + deepStrictEqual(executionSync.commandLine, expectedCommandLine); + await endEvent; + deepStrictEqual(executionSync.commandLine, expectedCommandLine); + await closeTerminalAsync(terminal); + }); + + test('executeCommand(executable, args)', async () => { + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo', ['hello']); + const executionSync = await execution; + const expectedCommandLine: TerminalShellExecutionCommandLine = { + value: 'echo "hello"', + isTrusted: true, + confidence: TerminalShellExecutionCommandLineConfidence.High + }; + deepStrictEqual(executionSync.commandLine, expectedCommandLine); + await endEvent; + deepStrictEqual(executionSync.commandLine, expectedCommandLine); + await closeTerminalAsync(terminal); + }); +}); 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 27e25dbf17f..7411d9c094a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -953,8 +953,8 @@ function sanitizeData(data: string): string { // Strip escape sequences so winpty/conpty doesn't cause flakiness, do for all platforms for // consistency - const terminalCodesRegex = /(?:\u001B|\u009B)[\[\]()#;?]*(?:(?:(?:[a-zA-Z0-9]*(?:;[a-zA-Z0-9]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-PR-TZcf-ntqry=><~]))/g; - data = data.replace(terminalCodesRegex, ''); + const CSI_SEQUENCE = /(:?(:?\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; + data = data.replace(CSI_SEQUENCE, ''); return data; } diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock index 3c35048cdc7..9999a40aa14 100644 --- a/extensions/vscode-api-tests/yarn.lock +++ b/extensions/vscode-api-tests/yarn.lock @@ -7,7 +7,159 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node-forge@^1.3.11": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.6.tgz#f3c19ffc98c2220e18de259bb172dd4d892a6075" + integrity sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw== + dependencies: + undici-types "~5.26.4" + +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +straightforward@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/straightforward/-/straightforward-4.2.2.tgz#a7d99b313dec5c04b0c637c7b8684dd44dc9167c" + integrity sha512-MxfuNnyTP4RPjadI3DkYIcNIp0DMXeDmAXY4/6QivU8lLIPGUqaS5VsEkaQ2QC+FICzc7QTb/lJPRIhGRKVuMA== + dependencies: + debug "^4.3.4" + yargs "^17.6.2" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.6.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index eb72136ccf4..159bd29573b 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -20,7 +20,7 @@ "jsonc-parser": "^3.2.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "contributes": { "semanticTokenTypes": [ diff --git a/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json index dac84162b3c..cc1450808af 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json @@ -1,7 +1,7 @@ [ { "c": "test1", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -15,7 +15,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": "dsd", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -71,7 +71,7 @@ }, { "c": "test2", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -85,7 +85,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "abc-def", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -141,7 +141,7 @@ }, { "c": "test-3", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -155,7 +155,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -169,7 +169,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "abcdef", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -211,7 +211,7 @@ }, { "c": "test-4", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -225,7 +225,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,7 +239,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -253,7 +253,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": "abc-def", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json index e1aa82e9ca3..c8c2d57d903 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json @@ -1,7 +1,7 @@ [ { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -15,7 +15,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": "blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -43,7 +43,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "a=\"brown,not_brown\"", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": "not_blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -127,7 +127,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "foo", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -169,7 +169,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -211,7 +211,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,7 +225,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,7 +239,7 @@ }, { "c": "foo=\"}\"", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -253,7 +253,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -281,7 +281,7 @@ }, { "c": "not_blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -295,7 +295,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +309,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -323,7 +323,7 @@ }, { "c": "1", - "t": "source.yaml constant.numeric.integer.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml constant.numeric.integer.decimal.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json index 2066b677e2c..e5267d62494 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json @@ -1,7 +1,7 @@ [ { "c": "swagger", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -15,7 +15,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -57,7 +57,7 @@ }, { "c": "2.0", - "t": "source.yaml string.quoted.single.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -71,7 +71,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "info", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -99,7 +99,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "description", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -141,7 +141,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -169,7 +169,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -183,7 +183,7 @@ }, { "c": "The API Management Service API defines an updated and refined version", - "t": "source.yaml string.quoted.single.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -196,8 +196,22 @@ } }, { - "c": " of the concepts currently known as Developer, APP, and API Product in Edge. Of", - "t": "source.yaml string.quoted.single.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -210,8 +224,8 @@ } }, { - "c": " note is the introduction of the API concept, missing previously from Edge", - "t": "source.yaml string.quoted.single.yaml", + "c": "of the concepts currently known as Developer, APP, and API Product in Edge. Of", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -224,8 +238,64 @@ } }, { - "c": " ", - "t": "source.yaml string.quoted.single.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.whitespace.separator.yaml", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.yaml: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.yaml: #0000FF", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string.quoted.single.yaml: #0F4A85", + "light_modern": "string.quoted.single.yaml: #0000FF" + } + }, + { + "c": "note is the introduction of the API concept, missing previously from Edge", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.yaml: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.yaml: #0000FF", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string.quoted.single.yaml: #0F4A85", + "light_modern": "string.quoted.single.yaml: #0000FF" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -239,7 +309,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -253,7 +323,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +337,7 @@ }, { "c": "title", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -281,7 +351,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -295,7 +365,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +379,7 @@ }, { "c": "API Management Service API", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -323,7 +393,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -337,7 +407,7 @@ }, { "c": "version", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -351,7 +421,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -365,7 +435,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -379,7 +449,7 @@ }, { "c": "initial", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json index 5948ea77fcd..cadc88d9671 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-173216_sh.json @@ -29,7 +29,7 @@ }, { "c": "declare", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell storage.modifier.declare.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell storage.modifier.declare.shell", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -43,7 +43,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": "-A", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.unquoted.argument.shell constant.other.option.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.unquoted.argument.shell constant.other.option.shell", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -71,7 +71,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -85,7 +85,7 @@ }, { "c": "juices", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -99,7 +99,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -113,7 +113,7 @@ }, { "c": "(", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.array.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": "[", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.bracket.named-array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "'apple'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.unquoted.shell entity.other.attribute-name.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.unquoted.shell entity.other.attribute-name.bracket.shell", "r": { "dark_plus": "entity.other.attribute-name: #9CDCFE", "light_plus": "entity.other.attribute-name: #E50000", @@ -169,7 +169,7 @@ }, { "c": "]", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.bracket.named-array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.begin.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -211,7 +211,7 @@ }, { "c": "Apple Juice", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.quoted.single.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -225,7 +225,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -239,7 +239,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -253,7 +253,7 @@ }, { "c": "[", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.bracket.named-array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": "'orange'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.unquoted.shell entity.other.attribute-name.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.unquoted.shell entity.other.attribute-name.bracket.shell", "r": { "dark_plus": "entity.other.attribute-name: #9CDCFE", "light_plus": "entity.other.attribute-name: #E50000", @@ -281,7 +281,7 @@ }, { "c": "]", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.bracket.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.bracket.named-array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -295,7 +295,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.assignment.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +309,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.begin.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -323,7 +323,7 @@ }, { "c": "Orange Juice", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.quoted.single.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -337,7 +337,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -351,7 +351,7 @@ }, { "c": ")", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell punctuation.definition.array.shell", + "t": "source.shell meta.statement.shell meta.expression.assignment.modified.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -490,8 +490,36 @@ } }, { - "c": "'apple'", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell meta.parameter-expansion", + "c": "'", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell meta.parameter-expansion string.quoted.single.shell punctuation.definition.string.begin.shell", + "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": "apple", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell meta.parameter-expansion string.quoted.single.shell", + "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.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell meta.parameter-expansion string.quoted.single.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-173224_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test-173224_sh.json index 2009a29baad..4c038d48fdd 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-173224_sh.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-173224_sh.json @@ -1,7 +1,7 @@ [ { "c": "alias", - "t": "source.shell meta.expression.assignment.shell storage.type.alias.shell", + "t": "source.shell meta.expression.assignment.alias.shell storage.type.alias.shell", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": " ", - "t": "source.shell meta.expression.assignment.shell", + "t": "source.shell meta.expression.assignment.alias.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": "brew_list", - "t": "source.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.expression.assignment.alias.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -43,7 +43,7 @@ }, { "c": "=", - "t": "source.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.expression.assignment.alias.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -57,7 +57,7 @@ }, { "c": "\"", - "t": "source.shell meta.expression.assignment.shell string.quoted.double.shell punctuation.definition.string.begin.shell", + "t": "source.shell meta.expression.assignment.alias.shell string.quoted.double.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -71,7 +71,7 @@ }, { "c": "brew leaves", - "t": "source.shell meta.expression.assignment.shell string.quoted.double.shell", + "t": "source.shell meta.expression.assignment.alias.shell string.quoted.double.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -85,7 +85,7 @@ }, { "c": "\"", - "t": "source.shell meta.expression.assignment.shell string.quoted.double.shell punctuation.definition.string.end.shell", + "t": "source.shell meta.expression.assignment.alias.shell string.quoted.double.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -99,7 +99,7 @@ }, { "c": "alias", - "t": "source.shell meta.expression.assignment.shell storage.type.alias.shell", + "t": "source.shell meta.expression.assignment.alias.shell storage.type.alias.shell", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -113,7 +113,7 @@ }, { "c": " ", - "t": "source.shell meta.expression.assignment.shell", + "t": "source.shell meta.expression.assignment.alias.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "brew-list", - "t": "source.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.expression.assignment.alias.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -141,7 +141,7 @@ }, { "c": "=", - "t": "source.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.expression.assignment.alias.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -155,7 +155,7 @@ }, { "c": "\"", - "t": "source.shell meta.expression.assignment.shell string.quoted.double.shell punctuation.definition.string.begin.shell", + "t": "source.shell meta.expression.assignment.alias.shell string.quoted.double.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -169,7 +169,7 @@ }, { "c": "brew leaves", - "t": "source.shell meta.expression.assignment.shell string.quoted.double.shell", + "t": "source.shell meta.expression.assignment.alias.shell string.quoted.double.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -183,7 +183,7 @@ }, { "c": "\"", - "t": "source.shell meta.expression.assignment.shell string.quoted.double.shell punctuation.definition.string.end.shell", + "t": "source.shell meta.expression.assignment.alias.shell string.quoted.double.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json index ef48d804f66..c1875bfa986 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json @@ -29,7 +29,7 @@ }, { "c": "cmd", - "t": "source.shell meta.statement.shell variable.other.assignment.shell", + "t": "source.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -43,7 +43,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell keyword.operator.assignment.shell", + "t": "source.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -57,7 +57,7 @@ }, { "c": "(", - "t": "source.shell meta.statement.shell punctuation.definition.array.shell", + "t": "source.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell", + "t": "source.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -85,7 +85,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.begin.shell", + "t": "source.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -99,7 +99,7 @@ }, { "c": "ls", - "t": "source.shell meta.statement.shell string.quoted.single.shell", + "t": "source.shell string.quoted.single.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -113,7 +113,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "t": "source.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -127,7 +127,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell", + "t": "source.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.begin.shell", + "t": "source.shell string.quoted.single.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -155,7 +155,7 @@ }, { "c": "-la", - "t": "source.shell meta.statement.shell string.quoted.single.shell", + "t": "source.shell string.quoted.single.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -169,7 +169,7 @@ }, { "c": "'", - "t": "source.shell meta.statement.shell string.quoted.single.shell punctuation.definition.string.end.shell", + "t": "source.shell string.quoted.single.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -183,7 +183,7 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell", + "t": "source.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": ")", - "t": "source.shell meta.statement.shell punctuation.definition.array.shell", + "t": "source.shell punctuation.definition.array.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -211,7 +211,7 @@ }, { "c": "if", - "t": "source.shell meta.scope.if-block.shell keyword.control.shell", + "t": "source.shell meta.scope.if-block.shell keyword.control.if.shell", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -239,7 +239,7 @@ }, { "c": "((", - "t": "source.shell meta.scope.if-block.shell meta.arithmetic.shell punctuation.section.arithmetic.shell", + "t": "source.shell meta.scope.if-block.shell meta.arithmetic.shell punctuation.section.arithmetic.double.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -337,16 +337,16 @@ }, { "c": "@", - "t": "source.shell meta.scope.if-block.shell meta.arithmetic.shell meta.parameter-expansion", + "t": "source.shell meta.scope.if-block.shell meta.arithmetic.shell meta.parameter-expansion keyword.operator.expansion.shell", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "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" } }, { @@ -393,7 +393,7 @@ }, { "c": "))", - "t": "source.shell meta.scope.if-block.shell meta.arithmetic.shell punctuation.section.arithmetic.shell", + "t": "source.shell meta.scope.if-block.shell meta.arithmetic.shell punctuation.section.arithmetic.double.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -533,16 +533,16 @@ }, { "c": "@", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell meta.statement.command.name.continuation string.quoted.double entity.name.function.call entity.name.command meta.parameter-expansion", + "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell meta.statement.command.name.continuation string.quoted.double entity.name.function.call entity.name.command meta.parameter-expansion keyword.operator.expansion.shell", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "entity.name.function: #DCDCAA", - "dark_modern": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #5E2CBC", - "light_modern": "entity.name.function: #795E26" + "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" } }, { @@ -757,16 +757,16 @@ }, { "c": "@", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell meta.parameter-expansion", + "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell meta.parameter-expansion keyword.operator.expansion.shell", "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" + "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" } }, { @@ -813,7 +813,7 @@ }, { "c": "fi", - "t": "source.shell meta.scope.if-block.shell keyword.control.shell", + "t": "source.shell meta.scope.if-block.shell keyword.control.fi.shell", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json b/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json index 856f013541b..3381f6448d0 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json @@ -686,7 +686,7 @@ } }, { - "c": " 5px", + "c": " ", "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less", "r": { "dark_plus": "default: #D4D4D4", @@ -699,6 +699,34 @@ "light_modern": "default: #3B3B3B" } }, + { + "c": "5", + "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less constant.numeric.less", + "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": "px", + "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less constant.numeric.less keyword.other.unit.less", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, { "c": ")", "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less punctuation.definition.group.end.less", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_go.json b/extensions/vscode-colorize-tests/test/colorize-results/test_go.json index 4d399b0c0ec..d6b2ef38ebb 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_go.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_go.json @@ -1065,7 +1065,7 @@ }, { "c": "nil", - "t": "source.go constant.language.go", + "t": "source.go constant.language.null.go", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json b/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json index 6889807ef11..deb5a66740a 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json @@ -2982,7 +2982,7 @@ } }, { - "c": "!==", + "c": "!=", "t": "source.julia keyword.operator.relation.julia", "r": { "dark_plus": "keyword.operator: #D4D4D4", @@ -2995,6 +2995,20 @@ "light_modern": "keyword.operator: #000000" } }, + { + "c": "=", + "t": "source.julia keyword.operator.update.julia", + "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.julia", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_less.json b/extensions/vscode-colorize-tests/test/colorize-results/test_less.json index 08d8b9f4fb5..a66224dd9e6 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_less.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_less.json @@ -897,16 +897,16 @@ }, { "c": " ", - "t": "source.css.less meta.property-list.less meta.property-value.less", + "t": "source.css.less meta.property-list.less meta.property-value.less variable.other.readwrite.less", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "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": "source.css variable: #9CDCFE", + "light_plus": "source.css variable: #E50000", + "dark_vs": "source.css variable: #9CDCFE", + "light_vs": "source.css variable: #E50000", + "hc_black": "source.css variable: #D4D4D4", + "dark_modern": "source.css variable: #9CDCFE", + "hc_light": "source.css variable: #264F78", + "light_modern": "source.css variable: #E50000" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json index 9323c747ca8..8f1cb4c388b 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json @@ -29,7 +29,7 @@ }, { "c": "if", - "t": "source.shell meta.scope.if-block.shell keyword.control.shell", + "t": "source.shell meta.scope.if-block.shell keyword.control.if.shell", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -967,7 +967,7 @@ }, { "c": "\t", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -981,7 +981,7 @@ }, { "c": "ROOT", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -995,7 +995,7 @@ }, { "c": "=", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1009,26 +1009,26 @@ }, { "c": "$(", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "dirname", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1037,40 +1037,40 @@ }, { "c": " ", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "dirname", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1079,40 +1079,40 @@ }, { "c": " ", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "realpath", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1121,21 +1121,21 @@ }, { "c": " ", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell punctuation.definition.string.begin.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell punctuation.definition.string.begin.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1149,7 +1149,7 @@ }, { "c": "$", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell punctuation.definition.variable.shell variable.parameter.positional.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell punctuation.definition.variable.shell variable.parameter.positional.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1163,7 +1163,7 @@ }, { "c": "0", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell variable.parameter.positional.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell variable.parameter.positional.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1177,7 +1177,7 @@ }, { "c": "\"", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell punctuation.definition.string.end.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell string.quoted.double.shell punctuation.definition.string.end.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1191,44 +1191,44 @@ }, { "c": ")", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { @@ -1247,7 +1247,7 @@ }, { "c": "\t", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1261,7 +1261,7 @@ }, { "c": "ROOT", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1275,7 +1275,7 @@ }, { "c": "=", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1289,26 +1289,26 @@ }, { "c": "$(", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "dirname", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1317,40 +1317,40 @@ }, { "c": " ", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "dirname", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1359,40 +1359,40 @@ }, { "c": " ", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "readlink", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1401,21 +1401,21 @@ }, { "c": " ", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1429,7 +1429,7 @@ }, { "c": "f", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1443,26 +1443,26 @@ }, { "c": " ", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell punctuation.definition.variable.shell variable.parameter.positional.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell punctuation.definition.variable.shell variable.parameter.positional.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -1471,12 +1471,12 @@ }, { "c": "0", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell variable.parameter.positional.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell variable.parameter.positional.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -1485,49 +1485,49 @@ }, { "c": ")", - "t": "source.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.scope.if-block.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.scope.if-block.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "fi", - "t": "source.shell meta.scope.if-block.shell keyword.control.shell", + "t": "source.shell meta.scope.if-block.shell keyword.control.fi.shell", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -1541,7 +1541,7 @@ }, { "c": "DEVELOPER", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.expression.assignment.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1555,7 +1555,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.expression.assignment.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1569,26 +1569,26 @@ }, { "c": "$(", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "xcode-select", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1597,21 +1597,21 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1625,7 +1625,7 @@ }, { "c": "print-path", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1639,21 +1639,21 @@ }, { "c": ")", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "LIPO", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.expression.assignment.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1667,7 +1667,7 @@ }, { "c": "=", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.expression.assignment.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1681,26 +1681,26 @@ }, { "c": "$(", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.begin.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "xcrun", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.statement.command.name.shell entity.name.function.call.shell entity.name.command.shell", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", + "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", @@ -1709,21 +1709,21 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1737,7 +1737,7 @@ }, { "c": "sdk", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1751,21 +1751,21 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "iphoneos", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.argument.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.argument.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1779,21 +1779,21 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument.shell constant.other.option.dash.shell", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1807,7 +1807,7 @@ }, { "c": "find", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell string.unquoted.argument constant.other.option", "r": { "dark_plus": "constant.other.option: #569CD6", "light_plus": "constant.other.option: #0000FF", @@ -1821,21 +1821,21 @@ }, { "c": " ", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": "lipo", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.argument.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.argument.shell", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1849,16 +1849,16 @@ }, { "c": ")", - "t": "source.shell meta.statement.shell meta.expression.assignment.shell string.interpolated.dollar.shell punctuation.definition.evaluation.parens.end.shell", + "t": "source.shell meta.expression.assignment.shell meta.scope.subshell punctuation.definition.subshell.single.shell", "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" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { @@ -1905,7 +1905,7 @@ }, { "c": "EOF", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell punctuation.definition.string.heredoc.shell", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell punctuation.definition.string.heredoc.delimiter.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1975,7 +1975,7 @@ }, { "c": "\t# A heredoc with a variable ", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent.EOF", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1989,7 +1989,7 @@ }, { "c": "$", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent punctuation.definition.variable.shell variable.other.normal.shell", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent.EOF punctuation.definition.variable.shell variable.other.normal.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2003,7 +2003,7 @@ }, { "c": "DEVELOPER", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent variable.other.normal.shell", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent.EOF variable.other.normal.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2017,7 +2017,7 @@ }, { "c": "\tsome more file", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell string.unquoted.heredoc.indent.EOF", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -2031,7 +2031,7 @@ }, { "c": "EOF", - "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell punctuation.definition.string.heredoc.shell", + "t": "source.shell meta.statement.shell meta.statement.command.shell meta.argument.shell punctuation.definition.string.heredoc.delimiter.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2479,7 +2479,7 @@ }, { "c": "export", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell storage.modifier.export.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.modified.shell storage.modifier.export.shell", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -2493,7 +2493,7 @@ }, { "c": " ", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.modified.shell", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2507,7 +2507,7 @@ }, { "c": "NODE_ENV", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.modified.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2521,7 +2521,7 @@ }, { "c": "=", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell keyword.operator.assignment.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.modified.shell keyword.operator.assignment.shell", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2535,7 +2535,7 @@ }, { "c": "development", - "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.shell variable.other.assignment.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.statement.shell meta.statement.shell meta.expression.assignment.modified.shell variable.other.assignment.shell", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2605,7 +2605,7 @@ }, { "c": "if", - "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.if.shell", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -3235,7 +3235,7 @@ }, { "c": "fi", - "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.shell", + "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.fi.shell", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json index 407cc7c7a1a..0908e19e3ea 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json @@ -1,7 +1,7 @@ [ { "c": "#", - "t": "source.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", + "t": "source.yaml meta.stream.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -15,7 +15,7 @@ }, { "c": " sequencer protocols for Laser eye surgery", - "t": "source.yaml comment.line.number-sign.yaml", + "t": "source.yaml meta.stream.yaml comment.line.number-sign.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -29,7 +29,7 @@ }, { "c": "---", - "t": "source.yaml entity.other.document.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml entity.other.document.begin.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -85,7 +85,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": "&", - "t": "source.yaml meta.property.yaml keyword.control.property.anchor.yaml punctuation.definition.anchor.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.anchor.yaml punctuation.definition.anchor.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -127,21 +127,21 @@ }, { "c": "id001", - "t": "source.yaml meta.property.yaml entity.name.type.anchor.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.anchor.yaml variable.other.anchor.yaml", "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" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" } }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "#", - "t": "source.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -169,7 +169,7 @@ }, { "c": " defines anchor label &id001", - "t": "source.yaml comment.line.number-sign.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -182,8 +182,22 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +211,7 @@ }, { "c": "instrument", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -211,7 +225,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,7 +239,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,7 +253,7 @@ }, { "c": "Lasik 2000", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -252,8 +266,22 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +295,7 @@ }, { "c": "pulseEnergy", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -281,7 +309,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -295,7 +323,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +337,7 @@ }, { "c": "5.4", - "t": "source.yaml constant.numeric.float.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml constant.numeric.float.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -322,8 +350,22 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_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.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -337,7 +379,7 @@ }, { "c": "spotSize", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -351,7 +393,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -365,7 +407,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -379,7 +421,7 @@ }, { "c": "1mm", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -393,7 +435,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -407,7 +449,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -421,7 +463,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -435,7 +477,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -449,7 +491,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -463,7 +505,7 @@ }, { "c": "*", - "t": "source.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -477,12 +519,12 @@ }, { "c": "id001", - "t": "source.yaml variable.other.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml variable.other.alias.yaml", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -491,7 +533,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -505,7 +547,7 @@ }, { "c": "#", - "t": "source.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -519,7 +561,7 @@ }, { "c": " refers to the first step (with anchor &id001)", - "t": "source.yaml comment.line.number-sign.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -533,7 +575,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -547,7 +589,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -561,7 +603,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -575,7 +617,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -589,7 +631,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -603,7 +645,7 @@ }, { "c": "*", - "t": "source.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -617,12 +659,12 @@ }, { "c": "id001", - "t": "source.yaml variable.other.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml variable.other.alias.yaml", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -630,8 +672,8 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -644,22 +686,8 @@ } }, { - "c": "spotSize", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", - "r": { - "dark_plus": "entity.name.tag: #569CD6", - "light_plus": "entity.name.tag: #800000", - "dark_vs": "entity.name.tag: #569CD6", - "light_vs": "entity.name.tag: #800000", - "hc_black": "entity.name.tag: #569CD6", - "dark_modern": "entity.name.tag: #569CD6", - "hc_light": "entity.name.tag: #0F4A85", - "light_modern": "entity.name.tag: #800000" - } - }, - { - "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -671,9 +699,23 @@ "light_modern": "default: #3B3B3B" } }, + { + "c": "spotSize:", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml invalid.illegal.unrecognized.yaml", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -687,7 +729,7 @@ }, { "c": "2mm", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -701,21 +743,21 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml punctuation.whitespace.separator.yaml", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "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": "string: #CE9178", + "light_plus": "string.unquoted.plain.out.yaml: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.unquoted.plain.out.yaml: #0000FF", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string.unquoted.plain.out.yaml: #0F4A85", + "light_modern": "string.unquoted.plain.out.yaml: #0000FF" } }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -729,7 +771,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -743,7 +785,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -757,7 +799,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -771,7 +813,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -785,7 +827,7 @@ }, { "c": "*", - "t": "source.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -799,12 +841,12 @@ }, { "c": "id002", - "t": "source.yaml variable.other.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml variable.other.alias.yaml", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -813,7 +855,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -827,7 +869,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -841,7 +883,7 @@ }, { "c": "{", - "t": "source.yaml meta.flow-mapping.yaml punctuation.definition.mapping.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.definition.mapping.begin.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -855,7 +897,7 @@ }, { "c": "name", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -869,7 +911,7 @@ }, { "c": ":", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -883,7 +925,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -897,7 +939,7 @@ }, { "c": "John Smith", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml string.unquoted.plain.in.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml string.unquoted.plain.in.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.in.yaml: #0000FF", @@ -911,7 +953,7 @@ }, { "c": ",", - "t": "source.yaml meta.flow-mapping.yaml punctuation.separator.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.separator.mapping.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -925,7 +967,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -939,7 +981,7 @@ }, { "c": "age", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -953,7 +995,7 @@ }, { "c": ":", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -967,7 +1009,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -981,7 +1023,7 @@ }, { "c": "33", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml constant.numeric.integer.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml string.unquoted.plain.in.yaml constant.numeric.integer.decimal.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -995,7 +1037,7 @@ }, { "c": "}", - "t": "source.yaml meta.flow-mapping.yaml punctuation.definition.mapping.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.definition.mapping.end.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1009,7 +1051,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1023,7 +1065,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1037,7 +1079,7 @@ }, { "c": "name", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1051,7 +1093,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1065,7 +1107,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1079,7 +1121,7 @@ }, { "c": "Mary Smith", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -1093,7 +1135,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1107,7 +1149,7 @@ }, { "c": "age", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1121,7 +1163,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1135,7 +1177,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1149,7 +1191,7 @@ }, { "c": "27", - "t": "source.yaml constant.numeric.integer.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml constant.numeric.integer.decimal.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1163,7 +1205,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1176,8 +1218,22 @@ } }, { - "c": "men", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "c": "m", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml invalid.illegal.expected-indentation.yaml", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, + { + "c": "en", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1191,7 +1247,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1205,7 +1261,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1219,7 +1275,7 @@ }, { "c": "[", - "t": "source.yaml meta.flow-sequence.yaml punctuation.definition.sequence.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.definition.sequence.begin.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1233,7 +1289,7 @@ }, { "c": "John Smith", - "t": "source.yaml meta.flow-sequence.yaml string.unquoted.plain.in.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml string.unquoted.plain.in.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.in.yaml: #0000FF", @@ -1247,7 +1303,7 @@ }, { "c": ",", - "t": "source.yaml meta.flow-sequence.yaml punctuation.separator.sequence.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.separator.sequence.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1261,7 +1317,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-sequence.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1275,7 +1331,7 @@ }, { "c": "Bill Jones", - "t": "source.yaml meta.flow-sequence.yaml string.unquoted.plain.in.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml string.unquoted.plain.in.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.in.yaml: #0000FF", @@ -1289,7 +1345,7 @@ }, { "c": "]", - "t": "source.yaml meta.flow-sequence.yaml punctuation.definition.sequence.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.definition.sequence.end.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1302,8 +1358,36 @@ } }, { - "c": "women", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "c": "w", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml invalid.illegal.expected-indentation.yaml", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, + { + "c": "o", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml invalid.illegal.expected-indentation.yaml", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, + { + "c": "men", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1317,7 +1401,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1331,7 +1415,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1345,7 +1429,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1359,7 +1443,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1373,7 +1457,7 @@ }, { "c": "Mary Smith", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -1387,7 +1471,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1401,7 +1485,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1415,7 +1499,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1429,7 +1513,7 @@ }, { "c": "Susan Williams", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", diff --git a/extensions/vscode-colorize-tests/yarn.lock b/extensions/vscode-colorize-tests/yarn.lock index a7a6fa446ca..88c52293616 100644 --- a/extensions/vscode-colorize-tests/yarn.lock +++ b/extensions/vscode-colorize-tests/yarn.lock @@ -2,12 +2,19 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index e538d43f21b..8ab2171ddaa 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -33,7 +33,7 @@ "main": "./out/extension", "browser": "./dist/browser/testResolverMain", "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "capabilities": { "untrustedWorkspaces": { diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 8e12e622e05..2fab3ec306a 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -164,8 +164,8 @@ export function activate(context: vscode.ExtensionContext) { const serverCommandPath = path.join(vscodePath, 'scripts', serverCommand); outputChannel.appendLine(`Launching server: "${serverCommandPath}" ${commandArgs.join(' ')}`); - - extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath }); + const shell = (process.platform === 'win32'); + extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath, shell }); } else { const extensionToInstall = process.env['TESTRESOLVER_INSTALL_BUILTIN_EXTENSION']; if (extensionToInstall) { @@ -182,8 +182,8 @@ export function activate(context: vscode.ExtensionContext) { outputChannel.appendLine(`Using server build at ${serverLocation}`); outputChannel.appendLine(`Server arguments ${commandArgs.join(' ')}`); - - extHostProcess = cp.spawn(path.join(serverLocation, 'bin', serverCommand), commandArgs, { env, cwd: serverLocation }); + const shell = (process.platform === 'win32'); + extHostProcess = cp.spawn(path.join(serverLocation, 'bin', serverCommand), commandArgs, { env, cwd: serverLocation, shell }); } extHostProcess.stdout!.on('data', (data: Buffer) => processOutput(data.toString())); extHostProcess.stderr!.on('data', (data: Buffer) => processOutput(data.toString())); diff --git a/extensions/vscode-test-resolver/yarn.lock b/extensions/vscode-test-resolver/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/vscode-test-resolver/yarn.lock +++ b/extensions/vscode-test-resolver/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/yaml/build/update-grammar.js b/extensions/yaml/build/update-grammar.js new file mode 100644 index 00000000000..8684bc3e5d0 --- /dev/null +++ b/extensions/yaml/build/update-grammar.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +var updateGrammar = require('vscode-grammar-updater'); + +async function updateGrammars() { + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.0.tmLanguage.json', './syntaxes/yaml-1.0.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.1.tmLanguage.json', './syntaxes/yaml-1.1.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.2.tmLanguage.json', './syntaxes/yaml-1.2.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.3.tmLanguage.json', './syntaxes/yaml-1.3.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml.tmLanguage.json', './syntaxes/yaml.tmLanguage.json', undefined, 'main'); +} + +updateGrammars(); diff --git a/extensions/yaml/cgmanifest.json b/extensions/yaml/cgmanifest.json index e6c3ca158b5..18882ead40a 100644 --- a/extensions/yaml/cgmanifest.json +++ b/extensions/yaml/cgmanifest.json @@ -4,33 +4,24 @@ "component": { "type": "git", "git": { - "name": "textmate/yaml.tmbundle", - "repositoryUrl": "https://github.com/textmate/yaml.tmbundle", - "commitHash": "e54ceae3b719506dba7e481a77cea4a8b576ae46" + "name": "RedCMD/YAML-Syntax-Highlighter", + "repositoryUrl": "https://github.com/RedCMD/YAML-Syntax-Highlighter", + "commitHash": "d4dca9f38a654ebbb13c1b72b7881e3c5864a778" } }, "licenseDetail": [ - "Copyright (c) 2015 FichteFoll ", + "MIT License", "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", + "Copyright 2024 RedCMD", "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "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." + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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." ], - "license": "TextMate Bundle License", - "version": "0.0.0" + "license": "MIT", + "version": "1.1.1" } ], "version": 1 diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index 5223f71c52b..2b0ee013964 100644 --- a/extensions/yaml/package.json +++ b/extensions/yaml/package.json @@ -9,7 +9,7 @@ "vscode": "*" }, "scripts": { - "update-grammar": "node ../node_modules/vscode-grammar-updater/bin textmate/yaml.tmbundle Syntaxes/YAML.tmLanguage ./syntaxes/yaml.tmLanguage.json" + "update-grammar": "node ./build/update-grammar.js" }, "categories": ["Programming Languages"], "contributes": { @@ -56,10 +56,32 @@ "scopeName": "source.yaml", "path": "./syntaxes/yaml.tmLanguage.json" }, + { + "scopeName": "source.yaml.1.3", + "path": "./syntaxes/yaml-1.3.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.2", + "path": "./syntaxes/yaml-1.2.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.1", + "path": "./syntaxes/yaml-1.1.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.0", + "path": "./syntaxes/yaml-1.0.tmLanguage.json" + }, { "language": "yaml", "scopeName": "source.yaml", - "path": "./syntaxes/yaml.tmLanguage.json" + "path": "./syntaxes/yaml.tmLanguage.json", + "unbalancedBracketScopes": [ + "invalid.illegal", + "meta.scalar.yaml", + "storage.type.tag.shorthand.yaml", + "keyword.control.flow" + ] } ], "configurationDefaults": { diff --git a/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json new file mode 100644 index 00000000000..7ae77112860 --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json @@ -0,0 +1,1150 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.0.tmLanguage.json", + "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/RedCMD/YAML-Syntax-Highlighter/commit/dfd7e5f4f71f9695c5d8697ca57f81240165aa04", + "name": "YAML 1.0", + "scopeName": "source.yaml.1.0", + "comment": "https://yaml.org/spec/1.0/", + "patterns": [ + { + "include": "#stream" + } + ], + "repository": { + "stream": { + "patterns": [ + { + "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", + "begin": "^(?!\\G)", + "while": "^", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "source.yaml.1.1#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "source.yaml.1.1#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML:1\\.0(?=[\\x{85 2028 2029}\r\n\t ]))", + "end": "\\G(?=%(?!YAML:1\\.0))", + "name": "meta.1.0.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)(:)(1\\.0)", + "while": "\\G(?!---[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "source.yaml.1.1#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "begin": "\\G(?=---[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!%)", + "patterns": [ + { + "include": "#document" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + "directives": { + "comment": "https://yaml.org/spec/1.2.2/#68-directives", + "patterns": [ + { + "include": "source.yaml.1.3#directive-YAML" + }, + { + "include": "source.yaml.1.2#directive-YAML" + }, + { + "include": "source.yaml.1.1#directive-YAML" + }, + { + "include": "source.yaml.1.0#directive-YAML" + }, + { + "begin": "(?=%)", + "while": "\\G(?!%|---[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.directives.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", + "begin": "(%)([^: \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.other.yaml" + } + }, + "patterns": [ + { + "match": "\\G(:)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + } + } + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + "document": { + "comment": "https://yaml.org/spec/1.2.2/#91-documents", + "patterns": [ + { + "begin": "---(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "entity.other.document.begin.yaml" + } + }, + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?=\\.{3}[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", + "patterns": [ + { + "begin": "\\G\\.{3}", + "end": "$", + "beginCaptures": { + "0": { + "name": "entity.other.document.end.yaml" + } + }, + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.1#byte-order-mark" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + }, + { + "include": "#block-scalar" + }, + { + "include": "source.yaml.1.1#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "source.yaml.1.1#single" + } + ] + }, + { + "begin": "(?={)", + "end": "$", + "patterns": [ + { + "include": "#flow-mapping" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.1#block-plain-out" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^\\x{85 2028 2029}}]++|}[ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^\\x{85 2028 2029}\\]]++|][ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++]|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?(\\1\\2)((?>[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "source.yaml.1.1#anchor-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "source.yaml.1.1#anchor-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "4": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "5": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.mapping.yaml", + "patterns": [ + { + "include": "#block-map-key-double" + }, + { + "include": "source.yaml#block-map-key-single" + }, + { + "include": "source.yaml.1.1#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" + }, + { + "include": "#block-map-value" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + "block-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", + "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\\x{85 2028 2029}\r\n\t ])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.block.sequence.item.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.block.sequence.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-map-key-explicit": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", + "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.map.key.yaml" + }, + "4": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.map.explicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "source.yaml.1.1#flow-key-plain-out" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-node" + } + ] + }, + "block-map-key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "//": "Assumming 3rd party preprocessing variables `{{...}}` turn into valid map-keys when inside a block-mapping", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ])|(?<=}})(?=[\t ]++#|[\t ]*+$)", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-scalar": { + "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", + "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((0)|(1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", + "while": "\\G(?>(?>(?!\\6)|(?!\\7) |(?!\\8) {2}|(?!\\9) {3}|(?!\\10) {4}|(?!\\11) {5}|(?!\\12) {6}|(?!\\13) {7}|(?!\\14) {8}|(?!\\15) {9})| *+($|[^#]))", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + }, + "5": { + "name": "constant.numeric.indentation-indicator.yaml" + }, + "16": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "whileCaptures": { + "0": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "1": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "begin": "$", + "while": "\\G", + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + }, + { + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", + "//": "Soooooooo many edge cases", + "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", + "while": "\\G", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", + "begin": "$", + "while": "\\G", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "//": "Find the highest indented line", + "begin": "\\G( ++)$", + "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", + "//": [ + "Funky wrapper function", + "The `end` pattern clears the parent `\\G` anchor", + "Affectively forcing this rule to only match at most once", + "https://github.com/microsoft/vscode-textmate/issues/114" + ], + "begin": "\\G(?!$)(?=( *+))", + "end": "\\G(?!\\1)(?=[\t ]*+#)", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "begin": "\\G( *+)", + "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", + "begin": "(?!\\G)(?=[\t ]*+#)", + "while": "\\G", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + { + "comment": "Header Comment", + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + } + ] + }, + "flow-node": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", + "patterns": [ + { + "begin": "(?=\\[|{)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "source.yaml.1.1#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "source.yaml.1.1#single" + } + ] + }, + { + "include": "source.yaml.1.1#flow-plain-in" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + "flow-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", + "begin": "{", + "end": "}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", + "begin": "(?<={)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.mapping.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", + "begin": "\\[", + "end": "]", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow.sequence.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", + "begin": "(?<=\\[)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.sequence.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#flow-sequence-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-mapping-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "source.yaml.1.1#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=\"|')", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + "flow-sequence-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?<=[\t ,\\[{]|^)(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + "flow-map-value-yaml": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-map-value-json": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", + "name": "invalid.illegal.constant.character.escape.yaml" + } + ] + }, + "tag-property": { + "comment": "https://yaml.org/spec/1.0/#c-ns-tag-property", + "//": [ + "!^", + "!!private_ns-tag-char+", + "!global_core_ns-tag-char+_no-:/!", + "!global_vocabulary_az09-_/ns-tag-char", + "!global_domain_ns-tag-char+.ns-tag-char+,1234(-12(-12)?)?/ns-tag-char*" + ], + "begin": "(?=!)", + "end": "(?=[\\x{2028 2029}\r\n\t ])", + "name": "storage.type.tag.yaml", + "patterns": [ + { + "match": "\\G!(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "punctuation.definition.tag.non-specific.yaml" + }, + { + "comment": "https://yaml.org/spec/1.0/#c-ns-private-tag", + "match": "\\G!!", + "name": "punctuation.definition.tag.private.yaml" + }, + { + "comment": "https://yaml.org/spec/1.0/#ns-ns-global-tag", + "match": "\\G!", + "name": "punctuation.definition.tag.global.yaml" + }, + { + "comment": "https://yaml.org/spec/1.0/#c-prefix", + "match": "\\^", + "name": "punctuation.definition.tag.prefix.yaml" + }, + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#double-escape" + }, + { + "include": "source.yaml#non-printable" + } + ] + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json new file mode 100644 index 00000000000..57a96ac1e4f --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json @@ -0,0 +1,1555 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.1.tmLanguage.json", + "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/RedCMD/YAML-Syntax-Highlighter/commit/dfd7e5f4f71f9695c5d8697ca57f81240165aa04", + "name": "YAML 1.1", + "scopeName": "source.yaml.1.1", + "comment": "https://yaml.org/spec/1.1/", + "patterns": [ + { + "include": "#stream" + } + ], + "repository": { + "stream": { + "patterns": [ + { + "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", + "begin": "^(?!\\G)", + "while": "^", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML[ \t]+1\\.1(?=[\\x{85 2028 2029}\r\n\t ]))", + "end": "\\G(?=%(?!YAML[ \t]+1\\.1))", + "name": "meta.1.1.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)([ \t]+)(1\\.1)", + "while": "\\G(?!---[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?=---[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!%)", + "patterns": [ + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + "directives": { + "comment": "https://yaml.org/spec/1.2.2/#68-directives", + "patterns": [ + { + "include": "source.yaml.1.3#directive-YAML" + }, + { + "include": "source.yaml.1.2#directive-YAML" + }, + { + "include": "source.yaml.1.1#directive-YAML" + }, + { + "include": "source.yaml.1.0#directive-YAML" + }, + { + "begin": "(?=%)", + "while": "\\G(?!%|---[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.directives.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#682-tag-directives", + "begin": "\\G(%)(TAG)(?>([\t ]++)((!)(?>[0-9A-Za-z-]*+(!))?+))?+", + "end": "$", + "applyEndPatternLast": true, + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.tag.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "storage.type.tag-handle.yaml" + }, + "5": { + "name": "punctuation.definition.tag.begin.yaml" + }, + "6": { + "name": "punctuation.definition.tag.end.yaml" + }, + "comment": "https://yaml.org/spec/1.2.2/#rule-c-tag-handle" + }, + "patterns": [ + { + "comment": "technically the beginning should only validate against a valid uri scheme [A-Za-z][A-Za-z0-9.+-]*", + "begin": "\\G[\t ]++(?!#)", + "end": "(?=[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "contentName": "support.type.tag-prefix.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "\\G[,\\[\\]{}]", + "name": "invalid.illegal.character.uri.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", + "begin": "(%)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.other.yaml" + } + }, + "patterns": [ + { + "match": "\\G([\t ]++)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + } + } + }, + { + "match": "([\t ]++)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-parameter.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-invalid": { + "patterns": [ + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "begin": "\\G(%)(YAML)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "invalid.illegal.keyword.other.directive.yaml.yaml" + } + }, + "name": "meta.directive.yaml", + "patterns": [ + { + "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "constant.numeric.yaml-version.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "document": { + "comment": "https://yaml.org/spec/1.2.2/#91-documents", + "patterns": [ + { + "begin": "---(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "entity.other.document.begin.yaml" + } + }, + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?=\\.{3}[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", + "patterns": [ + { + "begin": "\\G\\.{3}", + "end": "$", + "beginCaptures": { + "0": { + "name": "entity.other.document.end.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#byte-order-mark" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + }, + { + "include": "source.yaml.1.2#block-scalar" + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "begin": "(?={)", + "end": "$", + "patterns": [ + { + "include": "#flow-mapping" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#block-plain-out" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^\\x{85 2028 2029}}]++|}[ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^\\x{85 2028 2029}\\]]++|][ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++]|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?(\\1\\2)((?>[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "4": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "5": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.mapping.yaml", + "patterns": [ + { + "include": "#block-map-key-double" + }, + { + "include": "source.yaml#block-map-key-single" + }, + { + "include": "#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" + }, + { + "include": "#block-map-value" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", + "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\\x{85 2028 2029}\r\n\t ])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.block.sequence.item.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.block.sequence.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-map-key-explicit": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", + "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.map.key.yaml" + }, + "4": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.map.explicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-key-plain-out" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-node" + } + ] + }, + "block-map-key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "block-map-key-plain": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", + "begin": "\\G(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|(?>[\t ]++|\\G)#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G([\t ]++)(.)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "invalid.illegal.multiline-key.yaml" + } + } + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "//": "Assumming 3rd party preprocessing variables `{{...}}` turn into valid map-keys when inside a block-mapping", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ])|(?<=}})(?=[\t ]++#|[\t ]*+$)", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "while": "\\G", + "patterns": [ + { + "begin": "\\G", + "end": "(?=(?>[\t ]++|\\G)#)", + "name": "string.unquoted.plain.out.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": ":(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-node": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", + "patterns": [ + { + "begin": "(?=\\[|{)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "include": "#flow-plain-in" + }, + { + "include": "#presentation-detail" + } + ] + }, + "flow-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", + "begin": "{", + "end": "}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", + "begin": "(?<={)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.mapping.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", + "begin": "\\[", + "end": "]", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow.sequence.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", + "begin": "(?<=\\[)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.sequence.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-sequence-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-mapping-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=\"|')", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-sequence-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?<=[\t ,\\[{]|^)(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-value-yaml": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-map-value-json": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-IN)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", + "name": "string.unquoted.plain.in.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "flow-key-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|[\t ]++#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "flow-key-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", + "begin": "\\G(?![\\x{85 2028 2029}\r\n\t #])", + "end": "(?=[\t ]*+(?>:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", + "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", + "name": "invalid.illegal.constant.character.escape.yaml" + } + ] + }, + "tag-implicit-plain-in": { + "comment": "https://yaml.org/type/index.html", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE|y|Y|yes|Yes|YES|n|N|no|No|NO|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.binary.yaml" + }, + { + "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.decimal.yaml" + }, + { + "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.nan.yaml" + }, + { + "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", + "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.timestamp.yaml" + }, + { + "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.merge.yaml" + }, + { + "match": "\\G=(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.value.yaml" + }, + { + "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.yaml.yaml" + } + ] + }, + "tag-implicit-plain-out": { + "comment": "https://yaml.org/type/index.html", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE|yes|Yes|YES|y|Y|no|No|NO|n|N|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.binary.yaml" + }, + { + "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.decimal.yaml" + }, + { + "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.nan.yaml" + }, + { + "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", + "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.timestamp.yaml" + }, + { + "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.merge.yaml" + }, + { + "match": "\\G=(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.value.yaml" + }, + { + "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.yaml.yaml" + } + ] + }, + "tag-property": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-tag-property", + "//": [ + "!", + "!!", + "!<>", + "!...", + "!!...", + "!<...>", + "!...!..." + ], + "patterns": [ + { + "match": "!(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "storage.type.tag.non-specific.yaml punctuation.definition.tag.non-specific.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-verbatim-tag", + "begin": "!<", + "end": ">", + "beginCaptures": { + "0": { + "name": "punctuation.definition.tag.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag.end.yaml" + } + }, + "name": "storage.type.tag.verbatim.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-shorthand-tag", + "begin": "(?=!)", + "end": "(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "name": "storage.type.tag.shorthand.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G!!", + "name": "punctuation.definition.tag.secondary.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G(!)[0-9A-Za-z-]++(!)", + "captures": { + "1": { + "name": "punctuation.definition.tag.named.yaml" + }, + "2": { + "name": "punctuation.definition.tag.named.yaml" + } + } + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-primary-tag-handle", + "match": "\\G!", + "name": "punctuation.definition.tag.primary.yaml" + }, + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.~*'()\\[\\]%]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + } + ] + }, + "anchor-property": { + "match": "(&)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(&)", + "captures": { + "0": { + "name": "keyword.control.flow.anchor.yaml" + }, + "1": { + "name": "punctuation.definition.anchor.yaml" + }, + "2": { + "name": "variable.other.anchor.yaml" + }, + "3": { + "name": "invalid.illegal.flow.anchor.yaml" + } + } + }, + "alias": { + "begin": "(\\*)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(\\*)", + "end": "(?=:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", + "captures": { + "0": { + "name": "keyword.control.flow.alias.yaml" + }, + "1": { + "name": "punctuation.definition.alias.yaml" + }, + "2": { + "name": "variable.other.alias.yaml" + }, + "3": { + "name": "invalid.illegal.flow.alias.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + "byte-order-mark": { + "comment": "", + "begin": "\\G", + "while": "\\G(?=[\\x{FEFF 85 2028 2029}\r\n\t ])", + "patterns": [ + { + "begin": "(?=#)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G\\x{FEFF}", + "while": "\\G", + "beginCaptures": { + "0": { + "name": "byte-order-mark.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + "presentation-detail": { + "patterns": [ + { + "match": "[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "comment": "https://yaml.org/spec/1.1/#id871136", + "match": "[\\x{85 2028 2029}\r\n]++", + "name": "punctuation.separator.line-break.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "comment": { + "comment": "Comments must be separated from other tokens by white space characters. `space`, `newline` or `carriage-return`. `#(.*)` causes performance issues", + "begin": "(?<=^|[\\x{FEFF 85 2028 2029} ])#", + "end": "[\\x{85 2028 2029}\r\n]", + "captures": { + "0": { + "name": "punctuation.definition.comment.yaml" + } + }, + "name": "comment.line.number-sign.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\\x{85 2028 2029}#\"':,\\[\\]{}]&&!-~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml" + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json new file mode 100644 index 00000000000..965b6040816 --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json @@ -0,0 +1,1641 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.2.tmLanguage.json", + "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/RedCMD/YAML-Syntax-Highlighter/commit/d4dca9f38a654ebbb13c1b72b7881e3c5864a778", + "name": "YAML 1.2", + "scopeName": "source.yaml.1.2", + "comment": "https://yaml.org/spec/1.2.2", + "patterns": [ + { + "include": "#stream" + } + ], + "repository": { + "stream": { + "patterns": [ + { + "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", + "begin": "^(?!\\G)", + "while": "^", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "For when YAML is embedded inside a Markdown code-block", + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML[\t ]+1\\.2(?=[\r\n\t ]))", + "end": "\\G(?=(?>\\.{3}|---)[\r\n\t ])", + "name": "meta.1.2.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)([\t ]+)(1\\.2)", + "end": "\\G(?=---[\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#document" + } + ] + }, + "directives": { + "comment": "https://yaml.org/spec/1.2.2/#68-directives", + "patterns": [ + { + "include": "source.yaml.1.3#directive-YAML" + }, + { + "include": "source.yaml.1.2#directive-YAML" + }, + { + "include": "source.yaml.1.1#directive-YAML" + }, + { + "include": "source.yaml.1.0#directive-YAML" + }, + { + "begin": "(?=%)", + "while": "\\G(?!%|---[\r\n\t ])", + "name": "meta.directives.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#682-tag-directives", + "begin": "\\G(%)(TAG)(?>([\t ]++)((!)(?>[0-9A-Za-z-]*+(!))?+))?+", + "end": "$", + "applyEndPatternLast": true, + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.tag.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "storage.type.tag-handle.yaml" + }, + "5": { + "name": "punctuation.definition.tag.begin.yaml" + }, + "6": { + "name": "punctuation.definition.tag.end.yaml" + }, + "comment": "https://yaml.org/spec/1.2.2/#rule-c-tag-handle" + }, + "patterns": [ + { + "comment": "technically the beginning should only validate against a valid uri scheme [A-Za-z][A-Za-z0-9.+-]*", + "begin": "\\G[\t ]++(?!#)", + "end": "(?=[\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "contentName": "support.type.tag-prefix.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "\\G[,\\[\\]{}]", + "name": "invalid.illegal.character.uri.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", + "begin": "(%)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.other.yaml" + } + }, + "patterns": [ + { + "match": "\\G([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + } + } + }, + { + "match": "([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-parameter.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-invalid": { + "patterns": [ + { + "match": "\\G\\.{3}(?=[\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "begin": "\\G(%)(YAML)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "invalid.illegal.keyword.other.directive.yaml.yaml" + } + }, + "name": "meta.directive.yaml", + "patterns": [ + { + "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "constant.numeric.yaml-version.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "document": { + "comment": "https://yaml.org/spec/1.2.2/#91-documents", + "patterns": [ + { + "begin": "---(?=[\r\n\t ])", + "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", + "beginCaptures": { + "0": { + "name": "entity.other.document.begin.yaml" + } + }, + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?=\\.{3}[\r\n\t ])", + "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", + "patterns": [ + { + "begin": "\\G\\.{3}", + "end": "$", + "beginCaptures": { + "0": { + "name": "entity.other.document.end.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#byte-order-mark" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", + "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + }, + { + "include": "#block-scalar" + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "begin": "(?={)", + "end": "$", + "patterns": [ + { + "include": "#flow-mapping" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#block-plain-out" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?((?>[!&*][^\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^}]++|}[ \t]*+(?!:[\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^]]++|][ \t]*+(?!:[\r\n\t ]))++]|(?#Plain)(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))(?>[^:#]++|:(?![\r\n\t ])|(?(\\1\\2)((?>[!&*][^\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "4": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "5": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.mapping.yaml", + "patterns": [ + { + "include": "#block-map-key-double" + }, + { + "include": "source.yaml#block-map-key-single" + }, + { + "include": "#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" + }, + { + "include": "#block-map-value" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", + "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\r\n\t ])((?>\t[\t ]*+)?+[^\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.block.sequence.item.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.block.sequence.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-map-key-explicit": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", + "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\r\n\t ])", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]])((?>\t[\t ]*+)?+[^\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.map.key.yaml" + }, + "4": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.map.explicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-key-plain-out" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-node" + } + ] + }, + "block-map-key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "block-map-key-plain": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", + "begin": "\\G(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "end": "(?=[\t ]*+:[\r\n\t ]|(?>[\t ]++|\\G)#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G([\t ]++)(.)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "invalid.illegal.multiline-key.yaml" + } + } + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "//": "Assumming 3rd party preprocessing variables `{{...}}` turn into valid map-keys when inside a block-mapping", + "begin": ":(?=[\r\n\t ])|(?<=}})(?=[\t ]++#|[\t ]*+$)", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]]|-[^\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-scalar": { + "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", + "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", + "while": "\\G(?>(?>(?!\\6) |(?!\\7) {2}|(?!\\8) {3}|(?!\\9) {4}|(?!\\10) {5}|(?!\\11) {6}|(?!\\12) {7}|(?!\\13) {8}|(?!\\14) {9})| *+($|[^#]))", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + }, + "5": { + "name": "constant.numeric.indentation-indicator.yaml" + }, + "15": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "whileCaptures": { + "0": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "1": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "begin": "$", + "while": "\\G", + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + }, + { + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", + "//": "Soooooooo many edge cases", + "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", + "while": "\\G", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", + "begin": "$", + "while": "\\G", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "//": "Find the highest indented line", + "begin": "\\G( ++)$", + "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", + "//": [ + "Funky wrapper function", + "The `end` pattern clears the parent `\\G` anchor", + "Affectively forcing this rule to only match at most once", + "https://github.com/microsoft/vscode-textmate/issues/114" + ], + "begin": "\\G(?!$)(?=( *+))", + "end": "\\G(?!\\1)(?=[\t ]*+#)", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "begin": "\\G( *+)", + "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", + "begin": "(?!\\G)(?=[\t ]*+#)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "Header Comment", + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + } + ] + }, + "block-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "while": "\\G", + "patterns": [ + { + "begin": "\\G", + "end": "(?=(?>[\t ]++|\\G)#)", + "name": "string.unquoted.plain.out.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": ":(?=[\r\n\t ])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-node": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", + "patterns": [ + { + "begin": "(?=\\[|{)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "include": "#flow-plain-in" + }, + { + "include": "#presentation-detail" + } + ] + }, + "flow-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", + "begin": "{", + "end": "}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", + "begin": "(?<={)\\G(?=[\r\n\t ,#])|,", + "end": "(?=[^\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.mapping.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", + "begin": "\\[", + "end": "]", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow.sequence.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", + "begin": "(?<=\\[)\\G(?=[\r\n\t ,#])|,", + "end": "(?=[^\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.sequence.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-sequence-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-mapping-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=\"|')", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-sequence-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?<=[\t ,\\[{]|^)(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-value-yaml": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": ":(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-map-value-json": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-IN)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))", + "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", + "name": "string.unquoted.plain.in.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": ":(?=[\r\n\t ,\\[\\]{}])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "flow-key-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "end": "(?=[\t ]*+:[\r\n\t ]|[\t ]++#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "flow-key-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", + "begin": "\\G(?![\r\n\t #])", + "end": "(?=[\t ]*+(?>:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", + "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "source.yaml#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", + "name": "invalid.illegal.constant.character.escape.yaml" + } + ] + }, + "tag-implicit-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.yaml" + }, + { + "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.nan.yaml" + } + ] + }, + "tag-implicit-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.yaml" + }, + { + "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.nan.yaml" + } + ] + }, + "tag-property": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-tag-property", + "//": [ + "!", + "!!", + "!<>", + "!...", + "!!...", + "!<...>", + "!...!..." + ], + "patterns": [ + { + "match": "!(?=[\r\n\t ])", + "name": "storage.type.tag.non-specific.yaml punctuation.definition.tag.non-specific.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-verbatim-tag", + "begin": "!<", + "end": ">", + "beginCaptures": { + "0": { + "name": "punctuation.definition.tag.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag.end.yaml" + } + }, + "name": "storage.type.tag.verbatim.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-shorthand-tag", + "begin": "(?=!)", + "end": "(?=[\r\n\t ,\\[\\]{}])", + "name": "storage.type.tag.shorthand.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G!!", + "name": "punctuation.definition.tag.secondary.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G(!)[0-9A-Za-z-]++(!)", + "captures": { + "1": { + "name": "punctuation.definition.tag.named.yaml" + }, + "2": { + "name": "punctuation.definition.tag.named.yaml" + } + } + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-primary-tag-handle", + "match": "\\G!", + "name": "punctuation.definition.tag.primary.yaml" + }, + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$_.~*'()%]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + } + ] + }, + "anchor-property": { + "match": "(&)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(&)", + "captures": { + "0": { + "name": "keyword.control.flow.anchor.yaml" + }, + "1": { + "name": "punctuation.definition.anchor.yaml" + }, + "2": { + "name": "variable.other.anchor.yaml" + }, + "3": { + "name": "invalid.illegal.flow.anchor.yaml" + } + } + }, + "alias": { + "begin": "(\\*)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(\\*)", + "end": "(?=:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", + "captures": { + "0": { + "name": "keyword.control.flow.alias.yaml" + }, + "1": { + "name": "punctuation.definition.alias.yaml" + }, + "2": { + "name": "variable.other.alias.yaml" + }, + "3": { + "name": "invalid.illegal.flow.alias.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + "byte-order-mark": { + "comment": "", + "match": "\\G\\x{FEFF}++", + "name": "byte-order-mark.yaml" + }, + "presentation-detail": { + "patterns": [ + { + "match": "[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "source.yaml#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "comment": { + "comment": "Comments must be separated from other tokens by white space characters. `space`, `tab`, `newline` or `carriage-return`. `#(.*)` causes performance issues", + "begin": "(?<=[\\x{FEFF}\t ]|^)#", + "end": "\r|\n", + "captures": { + "0": { + "name": "punctuation.definition.comment.yaml" + } + }, + "name": "comment.line.number-sign.yaml", + "patterns": [ + { + "include": "source.yaml#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\"':,\\[\\]{}]&&!-~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml" + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json new file mode 100644 index 00000000000..8df69f61c8c --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json @@ -0,0 +1,60 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.3.tmLanguage.json", + "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/RedCMD/YAML-Syntax-Highlighter/commit/274009903e20ac6dc37ba5763fb853744e28c9b2", + "name": "YAML 1.3", + "scopeName": "source.yaml.1.3", + "comment": "https://spec.yaml.io/main/spec/1.3.0/", + "patterns": [ + { + "include": "source.yaml" + } + ], + "repository": { + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML[\t ]+1\\.3(?=[\r\n\t ]))", + "end": "\\G(?=(?>\\.{3}|---)[\r\n\t ])", + "name": "meta.1.3.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)([\t ]+)(1\\.3)", + "end": "\\G(?=---[\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "source.yaml.1.2#directive-invalid" + }, + { + "include": "source.yaml.1.2#directives" + }, + { + "include": "source.yaml.1.2#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.2#document" + } + ] + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml.tmLanguage.json b/extensions/yaml/syntaxes/yaml.tmLanguage.json index 447df713901..3a64fbd850e 100644 --- a/extensions/yaml/syntaxes/yaml.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml.tmLanguage.json @@ -1,621 +1,106 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/textmate/yaml.tmbundle/blob/master/Syntaxes/YAML.tmLanguage", + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml.tmLanguage.json", "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/textmate/yaml.tmbundle/commit/e54ceae3b719506dba7e481a77cea4a8b576ae46", - "name": "YAML", + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/d4dca9f38a654ebbb13c1b72b7881e3c5864a778", + "name": "YAML Ain't Markup Language", "scopeName": "source.yaml", "patterns": [ { - "include": "#comment" + "comment": "Support legacy FrontMatter integration", + "//": "https://github.com/microsoft/vscode-markdown-tm-grammar/pull/162", + "begin": "(?<=^-{3,}\\s*+)\\G$", + "while": "^(?! {3,0}-{3,}[ \t]*+$|[ \t]*+\\.{3}$)", + "patterns": [ + { + "include": "source.yaml.1.2" + } + ] }, { - "include": "#property" - }, - { - "include": "#directive" - }, - { - "match": "^---", - "name": "entity.other.document.begin.yaml" - }, - { - "match": "^\\.{3}", - "name": "entity.other.document.end.yaml" - }, - { - "include": "#node" + "comment": "Default to YAML version 1.2", + "include": "source.yaml.1.2" } ], "repository": { - "block-collection": { - "patterns": [ - { - "include": "#block-sequence" - }, - { - "include": "#block-mapping" - } - ] + "parity": { + "comment": "Yes... That is right. Due to the changes with \\x2028, \\x2029, \\x85 and 'tags'. This is all the code I was able to reuse between all YAML versions 1.3, 1.2, 1.1 and 1.0" }, - "block-mapping": { - "patterns": [ - { - "include": "#block-pair" - } - ] - }, - "block-node": { - "patterns": [ - { - "include": "#prototype" - }, - { - "include": "#block-scalar" - }, - { - "include": "#block-collection" - }, - { - "include": "#flow-scalar-plain-out" - }, - { - "include": "#flow-node" - } - ] - }, - "block-pair": { - "patterns": [ - { - "begin": "\\?", - "beginCaptures": { - "1": { - "name": "punctuation.definition.key-value.begin.yaml" - } - }, - "end": "(?=\\?)|^ *(:)|(:)", - "endCaptures": { - "1": { - "name": "punctuation.separator.key-value.mapping.yaml" - }, - "2": { - "name": "invalid.illegal.expected-newline.yaml" - } - }, - "name": "meta.block-mapping.yaml", - "patterns": [ - { - "include": "#block-node" - } - ] - }, - { - "begin": "(?x)\n (?=\n (?x:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n )\n (\n [^\\s:]\n | : \\S\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", - "patterns": [ - { - "include": "#flow-scalar-plain-out-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", - "beginCaptures": { - "0": { - "name": "entity.name.tag.yaml" - } - }, - "contentName": "entity.name.tag.yaml", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", - "name": "string.unquoted.plain.out.yaml" - } - ] - }, - { - "match": ":(?=\\s|$)", - "name": "punctuation.separator.key-value.mapping.yaml" - } - ] - }, - "block-scalar": { - "begin": "(?:(\\|)|(>))([1-9])?([-+])?(.*\\n?)", - "beginCaptures": { - "1": { - "name": "keyword.control.flow.block-scalar.literal.yaml" - }, - "2": { - "name": "keyword.control.flow.block-scalar.folded.yaml" - }, - "3": { - "name": "constant.numeric.indentation-indicator.yaml" - }, - "4": { - "name": "storage.modifier.chomping-indicator.yaml" - }, - "5": { - "patterns": [ - { - "include": "#comment" - }, - { - "match": ".+", - "name": "invalid.illegal.expected-comment-or-newline.yaml" - } - ] - } - }, - "end": "^(?=\\S)|(?!\\G)", - "patterns": [ - { - "begin": "^([ ]+)(?! )", - "end": "^(?!\\1|\\s*$)", - "name": "string.unquoted.block.yaml" - } - ] - }, - "block-sequence": { - "match": "(-)(?!\\S)", - "name": "punctuation.definition.block.sequence.item.yaml" - }, - "comment": { - "begin": "(?:(^[ \\t]*)|[ \\t]+)(?=#\\p{Print}*$)", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.yaml" - } - }, - "end": "(?!\\G)", - "patterns": [ - { - "begin": "#", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.yaml" - } - }, - "end": "\\n", - "name": "comment.line.number-sign.yaml" - } - ] - }, - "directive": { - "begin": "^%", - "beginCaptures": { - "0": { - "name": "punctuation.definition.directive.begin.yaml" - } - }, - "end": "(?=$|[ \\t]+($|#))", - "name": "meta.directive.yaml", - "patterns": [ - { - "captures": { - "1": { - "name": "keyword.other.directive.yaml.yaml" - }, - "2": { - "name": "constant.numeric.yaml-version.yaml" - } - }, - "match": "\\G(YAML)[ \\t]+(\\d+\\.\\d+)" - }, - { - "captures": { - "1": { - "name": "keyword.other.directive.tag.yaml" - }, - "2": { - "name": "storage.type.tag-handle.yaml" - }, - "3": { - "name": "support.type.tag-prefix.yaml" - } - }, - "match": "(?x)\n \\G\n (TAG)\n (?:[ \\t]+\n ((?:!(?:[0-9A-Za-z\\-]*!)?))\n (?:[ \\t]+ (\n ! (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )*\n | (?![,!\\[\\]{}]) (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+\n )\n )?\n )?\n " - }, - { - "captures": { - "1": { - "name": "support.other.directive.reserved.yaml" - }, - "2": { - "name": "string.unquoted.directive-name.yaml" - }, - "3": { - "name": "string.unquoted.directive-parameter.yaml" - } - }, - "match": "(?x) \\G (\\w+) (?:[ \\t]+ (\\w+) (?:[ \\t]+ (\\w+))? )?" - }, - { - "match": "\\S+", - "name": "invalid.illegal.unrecognized.yaml" - } - ] - }, - "flow-alias": { - "captures": { - "1": { - "name": "keyword.control.flow.alias.yaml" - }, - "2": { - "name": "punctuation.definition.alias.yaml" - }, - "3": { - "name": "variable.other.alias.yaml" - }, - "4": { - "name": "invalid.illegal.character.anchor.yaml" - } - }, - "match": "((\\*))([^\\s\\[\\]/{/},]+)([^\\s\\]},]\\S*)?" - }, - "flow-collection": { - "patterns": [ - { - "include": "#flow-sequence" - }, - { - "include": "#flow-mapping" - } - ] - }, - "flow-mapping": { - "begin": "\\{", - "beginCaptures": { - "0": { - "name": "punctuation.definition.mapping.begin.yaml" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.definition.mapping.end.yaml" - } - }, - "name": "meta.flow-mapping.yaml", - "patterns": [ - { - "include": "#prototype" - }, - { - "match": ",", - "name": "punctuation.separator.mapping.yaml" - }, - { - "include": "#flow-pair" - } - ] - }, - "flow-node": { - "patterns": [ - { - "include": "#prototype" - }, - { - "include": "#flow-alias" - }, - { - "include": "#flow-collection" - }, - { - "include": "#flow-scalar" - } - ] - }, - "flow-pair": { - "patterns": [ - { - "begin": "\\?", - "beginCaptures": { - "0": { - "name": "punctuation.definition.key-value.begin.yaml" - } - }, - "end": "(?=[},\\]])", - "name": "meta.flow-pair.explicit.yaml", - "patterns": [ - { - "include": "#prototype" - }, - { - "include": "#flow-pair" - }, - { - "include": "#flow-node" - }, - { - "begin": ":(?=\\s|$|[\\[\\]{},])", - "beginCaptures": { - "0": { - "name": "punctuation.separator.key-value.mapping.yaml" - } - }, - "end": "(?=[},\\]])", - "patterns": [ - { - "include": "#flow-value" - } - ] - } - ] - }, - { - "begin": "(?x)\n (?=\n (?:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n )\n (\n [^\\s:[\\[\\]{},]]\n | : [^\\s[\\[\\]{},]]\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", - "name": "meta.flow-pair.key.yaml", - "patterns": [ - { - "include": "#flow-scalar-plain-in-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", - "beginCaptures": { - "0": { - "name": "entity.name.tag.yaml" - } - }, - "contentName": "entity.name.tag.yaml", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", - "name": "string.unquoted.plain.in.yaml" - } - ] - }, - { - "include": "#flow-node" - }, - { - "begin": ":(?=\\s|$|[\\[\\]{},])", - "captures": { - "0": { - "name": "punctuation.separator.key-value.mapping.yaml" - } - }, - "end": "(?=[},\\]])", - "name": "meta.flow-pair.yaml", - "patterns": [ - { - "include": "#flow-value" - } - ] - } - ] - }, - "flow-scalar": { - "patterns": [ - { - "include": "#flow-scalar-double-quoted" - }, - { - "include": "#flow-scalar-single-quoted" - }, - { - "include": "#flow-scalar-plain-in" - } - ] - }, - "flow-scalar-double-quoted": { - "begin": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "end": "\"", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "string.quoted.double.yaml", - "patterns": [ - { - "match": "\\\\([0abtnvfre \"/\\\\N_Lp]|x\\d\\d|u\\d{4}|U\\d{8})", - "name": "constant.character.escape.yaml" - }, - { - "match": "\\\\\\n", - "name": "constant.character.escape.double-quoted.newline.yaml" - } - ] - }, - "flow-scalar-plain-in": { - "patterns": [ - { - "include": "#flow-scalar-plain-in-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", - "name": "string.unquoted.plain.in.yaml" - } - ] - }, - "flow-scalar-plain-in-implicit-type": { - "patterns": [ - { - "captures": { - "1": { - "name": "constant.language.null.yaml" - }, - "2": { - "name": "constant.language.boolean.yaml" - }, - "3": { - "name": "constant.numeric.integer.yaml" - }, - "4": { - "name": "constant.numeric.float.yaml" - }, - "5": { - "name": "constant.other.timestamp.yaml" - }, - "6": { - "name": "constant.language.value.yaml" - }, - "7": { - "name": "constant.language.merge.yaml" - } - }, - "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n )\n " - } - ] - }, - "flow-scalar-plain-out": { - "patterns": [ - { - "include": "#flow-scalar-plain-out-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", - "name": "string.unquoted.plain.out.yaml" - } - ] - }, - "flow-scalar-plain-out-implicit-type": { - "patterns": [ - { - "captures": { - "1": { - "name": "constant.language.null.yaml" - }, - "2": { - "name": "constant.language.boolean.yaml" - }, - "3": { - "name": "constant.numeric.integer.yaml" - }, - "4": { - "name": "constant.numeric.float.yaml" - }, - "5": { - "name": "constant.other.timestamp.yaml" - }, - "6": { - "name": "constant.language.value.yaml" - }, - "7": { - "name": "constant.language.merge.yaml" - } - }, - "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?x:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n )\n " - } - ] - }, - "flow-scalar-single-quoted": { - "begin": "'", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, + "block-map-key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, "endCaptures": { "0": { "name": "punctuation.definition.string.end.yaml" } }, - "name": "string.quoted.single.yaml", + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, { "match": "''", - "name": "constant.character.escape.single-quoted.yaml" + "name": "constant.character.escape.single-quote.yaml" } ] }, - "flow-sequence": { - "begin": "\\[", + "key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (FLOW-OUT)", + "begin": "'", + "end": "'(?!')", "beginCaptures": { "0": { - "name": "punctuation.definition.sequence.begin.yaml" + "name": "punctuation.definition.string.begin.yaml" } }, - "end": "\\]", "endCaptures": { "0": { - "name": "punctuation.definition.sequence.end.yaml" + "name": "punctuation.definition.string.end.yaml" } }, - "name": "meta.flow-sequence.yaml", + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", "patterns": [ { - "include": "#prototype" + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" }, { - "match": ",", - "name": "punctuation.separator.sequence.yaml" - }, - { - "include": "#flow-pair" - }, - { - "include": "#flow-node" + "match": "''", + "name": "constant.character.escape.single-quote.yaml" } ] }, - "flow-value": { - "patterns": [ - { - "begin": "\\G(?![},\\]])", - "end": "(?=[},\\]])", - "name": "meta.flow-pair.value.yaml", - "patterns": [ - { - "include": "#flow-node" - } - ] - } - ] - }, - "node": { - "patterns": [ - { - "include": "#block-node" - } - ] - }, - "property": { - "begin": "(?=!|&)", - "end": "(?!\\G)", - "name": "meta.property.yaml", - "patterns": [ - { - "captures": { - "1": { - "name": "keyword.control.property.anchor.yaml" - }, - "2": { - "name": "punctuation.definition.anchor.yaml" - }, - "3": { - "name": "entity.name.type.anchor.yaml" - }, - "4": { - "name": "invalid.illegal.character.anchor.yaml" - } - }, - "match": "\\G((&))([^\\s\\[\\]/{/},]+)(\\S+)?" - }, - { - "match": "(?x)\n \\G\n (?:\n ! < (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+ >\n | (?:!(?:[0-9A-Za-z\\-]*!)?) (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$_.~*'()] )+\n | !\n )\n (?=\\ |\\t|$)\n ", - "name": "storage.type.tag-handle.yaml" - }, - { - "match": "\\S+", - "name": "invalid.illegal.tag-handle.yaml" - } - ] - }, - "prototype": { - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#property" - } - ] + "non-printable": { + "//": { + "85": "…", + "2028": "", + "2029": "", + "10000": "𐀀", + "A0": " ", + "D7FF": "퟿", + "E000": "", + "FFFD": "�", + "FEFF": "", + "FFFF": "￿", + "10FFFF": "􏿿" + }, + "//match": "[\\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}&&[^\t\n\r\\x{85}]]++", + "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", + "name": "invalid.illegal.non-printable.yaml" } } } \ No newline at end of file diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 23ab31c1254..962d9f4b001 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,120 +2,125 @@ # yarn lockfile v1 -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== + +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== "@parcel/watcher@2.1.0": version "2.1.0" @@ -128,11 +133,11 @@ node-gyp-build "^4.3.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" coffeescript@1.12.7: version "1.12.7" @@ -146,44 +151,45 @@ cson-parser@^4.0.9: dependencies: coffeescript "1.12.7" -esbuild@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== +esbuild@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" fast-plist@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -217,10 +223,10 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== picomatch@^2.3.1: version "2.3.1" @@ -234,10 +240,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@5.4: - version "5.4.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" - integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index 2a2a6b55e37..a8a8faa6be1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.88.0", - "distro": "340432a8308f66007779ec2133ee39e6995006cb", + "version": "1.93.0", + "distro": "f89267a3624018b4cc47e0f11a5430e2ac1642f0", "author": { "name": "Microsoft Corporation" }, @@ -52,7 +52,7 @@ "watch-cli": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js watch-cli", "eslint": "node build/eslint", "stylelint": "node build/stylelint", - "playwright-install": "node build/azure-pipelines/common/installPlaywright.js", + "playwright-install": "yarn playwright install", "compile-build": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js compile-build", "compile-extensions-build": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js compile-extensions-build", "minify-vscode": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode", @@ -63,43 +63,46 @@ "core-ci-pr": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js core-ci-pr", "extensions-ci": "node --max-old-space-size=8095 ./node_modules/gulp/bin/gulp.js extensions-ci", "extensions-ci-pr": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci-pr", - "perf": "node scripts/code-perf.js" + "perf": "node scripts/code-perf.js", + "update-build-ts-version": "yarn add typescript@next && tsc -p ./build/tsconfig.build.json" }, "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", - "@vscode/proxy-agent": "^0.19.0", + "@vscode/proxy-agent": "^0.22.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", "@vscode/sudo-prompt": "9.3.1", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.7.0-beta.12", - "@xterm/addon-image": "0.8.0-beta.12", - "@xterm/addon-search": "0.15.0-beta.12", - "@xterm/addon-serialize": "0.13.0-beta.12", - "@xterm/addon-unicode11": "0.8.0-beta.12", - "@xterm/addon-webgl": "0.18.0-beta.12", - "@xterm/headless": "5.5.0-beta.12", - "@xterm/xterm": "5.5.0-beta.12", - "graceful-fs": "4.2.11", + "@xterm/addon-clipboard": "0.2.0-beta.35", + "@xterm/addon-image": "0.9.0-beta.52", + "@xterm/addon-search": "0.16.0-beta.52", + "@xterm/addon-serialize": "0.14.0-beta.52", + "@xterm/addon-unicode11": "0.9.0-beta.52", + "@xterm/addon-webgl": "0.19.0-beta.52", + "@xterm/headless": "5.6.0-beta.52", + "@xterm/xterm": "5.6.0-beta.52", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "jschardet": "3.0.0", - "kerberos": "^2.0.1", + "jschardet": "3.1.3", + "kerberos": "2.1.1-alpha.0", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", - "native-keymap": "^3.3.4", + "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta11", - "tas-client-umd": "0.1.8", - "v8-inspect-profiler": "^0.1.0", + "node-pty": "1.1.0-beta19", + "open": "^8.4.2", + "tas-client-umd": "0.2.0", + "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", @@ -107,37 +110,35 @@ "yazl": "^2.4.3" }, "devDependencies": { - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.45.0", "@swc/core": "1.3.62", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", - "@types/graceful-fs": "4.1.2", - "@types/gulp-postcss": "^8.0.6", "@types/gulp-svgmin": "^1.2.1", "@types/http-proxy-agent": "^2.0.1", "@types/kerberos": "^1.1.2", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^1.0.6", "@types/vscode-notebook-renderer": "^1.72.0", - "@types/webpack": "^5.28.1", + "@types/webpack": "^5.28.5", "@types/wicg-file-system-access": "^2020.9.6", "@types/windows-foreground-love": "^0.3.0", "@types/winreg": "^1.2.30", "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", - "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/experimental-utils": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", + "@typescript-eslint/parser": "^6.21.0", "@vscode/gulp-electron": "^1.36.0", - "@vscode/l10n-dev": "0.0.30", + "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", "@vscode/test-cli": "^0.0.6", - "@vscode/test-electron": "^2.3.8", - "@vscode/test-web": "^0.0.50", + "@vscode/test-electron": "^2.4.0", + "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", "ansi-colors": "^3.2.3", @@ -149,7 +150,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "28.2.8", + "electron": "30.1.2", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", @@ -170,7 +171,6 @@ "gulp-gzip": "^1.4.2", "gulp-json-editor": "^2.5.0", "gulp-plumber": "^1.2.0", - "gulp-postcss": "^9.1.0", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.4", "gulp-sourcemaps": "^3.0.0", @@ -209,16 +209,18 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240318", - "typescript-formatter": "7.1.0", + "typescript": "^5.6.0-dev.20240805", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", - "webpack": "^5.77.0", - "webpack-cli": "^5.0.1", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, + "resolutions": { + "node-gyp-build": "4.8.1" + }, "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" diff --git a/product.json b/product.json index 36ca2626813..1803992a004 100644 --- a/product.json +++ b/product.json @@ -34,8 +34,8 @@ "builtInExtensions": [ { "name": "ms-vscode.js-debug-companion", - "version": "1.1.2", - "sha256": "e034b8b41beb4e97e02c70f7175bd88abe66048374c2bd629f54bb33354bc2aa", + "version": "1.1.3", + "sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -50,8 +50,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.88.0", - "sha256": "3a048d87c0fac116fce9190ff6042ec7691e66fc17211a058faf0db91bb6605e", + "version": "1.92.0", + "sha256": "e5d0a74728292423631f79d076ecb2bc129f9637bcbc2529e48a0fd53baa69cc", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", @@ -66,8 +66,8 @@ }, { "name": "ms-vscode.vscode-js-profile-table", - "version": "1.0.8", - "sha256": "ca30069e21fbf576b49638ff8ff7c316b028c2faca924c23526737c65b8762bf", + "version": "1.0.9", + "sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6", "repo": "https://github.com/microsoft/vscode-js-profile-visualizer", "metadata": { "id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb", diff --git a/remote/.yarnrc b/remote/.yarnrc index 4e7208cdf69..8d07643c18e 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" -target "18.18.2" -ms_build_id "256117" +target "20.14.0" +ms_build_id "282653" runtime "node" build_from_source "true" diff --git a/remote/package.json b/remote/package.json index 974450e8fea..0f3c8d72d22 100644 --- a/remote/package.json +++ b/remote/package.json @@ -6,35 +6,39 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.19.0", + "@vscode/proxy-agent": "^0.22.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.7.0-beta.12", - "@xterm/addon-image": "0.8.0-beta.12", - "@xterm/addon-search": "0.15.0-beta.12", - "@xterm/addon-serialize": "0.13.0-beta.12", - "@xterm/addon-unicode11": "0.8.0-beta.12", - "@xterm/addon-webgl": "0.18.0-beta.12", - "@xterm/headless": "5.5.0-beta.12", - "@xterm/xterm": "5.5.0-beta.12", + "@xterm/addon-clipboard": "0.2.0-beta.35", + "@xterm/addon-image": "0.9.0-beta.52", + "@xterm/addon-search": "0.16.0-beta.52", + "@xterm/addon-serialize": "0.14.0-beta.52", + "@xterm/addon-unicode11": "0.9.0-beta.52", + "@xterm/addon-webgl": "0.19.0-beta.52", + "@xterm/headless": "5.6.0-beta.52", + "@xterm/xterm": "5.6.0-beta.52", "cookie": "^0.4.0", - "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "jschardet": "3.0.0", - "kerberos": "^2.0.1", + "jschardet": "3.1.3", + "kerberos": "2.1.1-alpha.0", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta11", - "tas-client-umd": "0.1.8", + "node-pty": "1.1.0-beta19", + "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", "yauzl": "^3.0.0", "yazl": "^2.4.3" + }, + "resolutions": { + "node-gyp-build": "4.8.1" } } diff --git a/remote/web/package.json b/remote/web/package.json index 8b140d2171c..c7fdaf96b33 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -6,16 +6,17 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/tree-sitter-wasm": "^0.0.1", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.7.0-beta.12", - "@xterm/addon-image": "0.8.0-beta.12", - "@xterm/addon-search": "0.15.0-beta.12", - "@xterm/addon-serialize": "0.13.0-beta.12", - "@xterm/addon-unicode11": "0.8.0-beta.12", - "@xterm/addon-webgl": "0.18.0-beta.12", - "@xterm/xterm": "5.5.0-beta.12", - "jschardet": "3.0.0", - "tas-client-umd": "0.1.8", + "@xterm/addon-clipboard": "0.2.0-beta.35", + "@xterm/addon-image": "0.9.0-beta.52", + "@xterm/addon-search": "0.16.0-beta.52", + "@xterm/addon-serialize": "0.14.0-beta.52", + "@xterm/addon-unicode11": "0.9.0-beta.52", + "@xterm/addon-webgl": "0.19.0-beta.52", + "@xterm/xterm": "5.6.0-beta.52", + "jschardet": "3.1.3", + "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-textmate": "9.0.0" } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 4c55cada148..cdc4d7918b8 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -43,55 +43,67 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== +"@vscode/tree-sitter-wasm@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.1.tgz#ffb2e295a416698f4c77cbffeca3b28567d6754b" + integrity sha512-m0GKnQ3BxWnVd+20KLGwr1+Qvt/RiiaJmKAqHNU35pNydDtduUzyBm7ETz/T0vOVKoeIAaiYsJOA1aKWs7Y1tA== + "@vscode/vscode-languagedetection@1.0.21": version "1.0.21" resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-canvas@0.7.0-beta.12": - version "0.7.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.12.tgz#200f0293c507b75064963b5fc72a115799533920" - integrity sha512-euzQyWdklaSxzmb87kuwwiVP06vuYe1oUK+CiQW24UggSXThOEvZhvYV3O6iEgLe3p+7QfgnRWohXhCM84VOew== +"@xterm/addon-clipboard@0.2.0-beta.35": + version "0.2.0-beta.35" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.35.tgz#4c9b553ea63ce02a3c8075fea7df1637d52092ef" + integrity sha512-B8AulZEjsfvSEaLKp8oyRu7yJ7FJb5R3W0wpPbI/rOMVAuBwxDJsz0CxLvJUXnJX7OJwd5cjnyTnEcXJfMJycA== + dependencies: + js-base64 "^3.7.5" -"@xterm/addon-image@0.8.0-beta.12": - version "0.8.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.12.tgz#3fc5cea489d7159bf496b3a6d6515a109dab7226" - integrity sha512-YsBhmzwxRmym2dUA2CSm52Wt3OLhydVHM+SZmRAJ0/hvfB7dDjtuXBUSIdQWB16WWbGdi4Iazcs/TTxtarX/yA== +"@xterm/addon-image@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.52.tgz#a3115a50f884e5ba2f8ce09118a3d7e833fceb7b" + integrity sha512-1fWhnCIvLeO0aQ3CKqkTB9ye1bUsocpgFdDOgmwfW4XhLXpvu+QcyMGQMtWJHt8JWBN2w0cgR9eyfKw7orN+9Q== -"@xterm/addon-search@0.15.0-beta.12": - version "0.15.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.12.tgz#0c54677512135bbf820e3949c9dabeacc690d495" - integrity sha512-63ZhxXj6jBYumVrWJ7ZssICSMz+jHsXbi67tDQNMwTRO/MJxTittZeTHQ7IQrRYzKQgixrX0rLH7AwrLBrn2uQ== +"@xterm/addon-search@0.16.0-beta.52": + version "0.16.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.52.tgz#f8c77629b95ceff7d6b93d95c4b085857f405470" + integrity sha512-ZLVh0O91dcjxCjrU3vadl+40Z/mBnYXhKNA58oU/dGWFtFxtUB9SaZoOUtBvnfDpQIloYAK6raC2AfVsKHzD8A== -"@xterm/addon-serialize@0.13.0-beta.12": - version "0.13.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.12.tgz#438c1a6249cf4da3773d3de11a56638fa88d752a" - integrity sha512-/32Gpcj37Ftqf6b4+H62rcB70jLXi9IQspod/2mK3K+Yza9X+Yc8VkAz8VgpKa6tzbh3Xk0XEo/dB6kVFv1Jsg== +"@xterm/addon-serialize@0.14.0-beta.52": + version "0.14.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.52.tgz#19708cdd2895ddbd983b771ae9d14d7bc54cf7c9" + integrity sha512-1+ckKya1OURFmELH1Tjjoxz3Gnj78Dxj+NNRrEunfINkvyzaY+n8wT28FQxIlU5gJq+a0VGvlhNgTkMwgOn6aw== -"@xterm/addon-unicode11@0.8.0-beta.12": - version "0.8.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.12.tgz#7066297e2c662f6c235a4814e337c5a7fc8a91e2" - integrity sha512-uNsWmRpl4LaBfykpP9CKMo+49gVxRxHoC5MFuMhqPPNhXShsdBii3YxglwoKtit1fwzVT0CIWEniZQMlGiTIuw== +"@xterm/addon-unicode11@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.52.tgz#15afdf03c20d2e46b4eca68bd461eea71c8dd37f" + integrity sha512-5tZR/8c+vf0YNSYS6B9pEv8gyWWZpPYOf/BRQDkTGtYAnFf04MzggVE/U7tKUXGDzBhzwTPODq5qPNTX1xpGgw== -"@xterm/addon-webgl@0.18.0-beta.12": - version "0.18.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.12.tgz#410396fb6a3edd033eb16f334b88f31870952afb" - integrity sha512-wnIf5Xv0qAWQ0I1G5drKpEThA+D0f03iOTdtPR3uSLDfR8OsmpnSRgiR0Y0nAOnDmiCnDxu/wdBCKOAcXhWl2Q== +"@xterm/addon-webgl@0.19.0-beta.52": + version "0.19.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.52.tgz#695b20a5fa88ff92e115624149592080fad59594" + integrity sha512-kbPO9iR166xW8qgRkYmKX2Vu0kQHXpxYLQ9jY/01e5kvNrI/rqRDV63FIq14ncOi7N3+dmTuUkjvbg8anCpuIw== -"@xterm/xterm@5.5.0-beta.12": - version "5.5.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.12.tgz#a0f7445a10f958a4949fbe6989693c93e9e0265e" - integrity sha512-+I/vQh16ndYt8erj7zrxywPb+niyZC1W0H0w/ueDB3IPC7zPXxcETR0OGmglL7kq8Erb76ukBYXw9byXR2vtxg== +"@xterm/xterm@5.6.0-beta.52": + version "5.6.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.52.tgz#ed7456a8b78ea1d00431737d078b5ab0bafbcdfe" + integrity sha512-aZh8IBdZPb2N4NjTt/fQ/C90z/PM3Zxkfoqhmlsp5+v3Otmyw5kd3DepeHK1SFW/pz0/xdj4KFgX8t8Y2lDRbA== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== -tas-client-umd@0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.8.tgz#38bd32d49545417a0ea67fb618e646298e1b67cc" - integrity sha512-0jAAujLmjjGXf9PzrNpjOrr/6CTpSOp8jX80NOHK5nlOTWWpwaZ16EOyrPdHnm2bVfPHvT0/RAD0xyiQHGQvCQ== +jschardet@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.3.tgz#10c2289fdae91a0aa9de8bba9c59055fd78898d3" + integrity sha512-Q1PKVMK/uu+yjdlobgWIYkUOCR1SqUmW9m/eUJNNj4zI2N12i25v8fYpVf+zCakQeaTdBdhnZTFbVIAVZIVVOg== + +tas-client-umd@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.2.0.tgz#b71cc28f4c9b14f7b62f1ca4669886aa197e390c" + integrity sha512-oezN7mJVm5qZDVEby7OzxCLKUpUN5of0rY4dvOWaDF2JZBlGpd3BXceFN8B53qlTaIkVSzP65aAMT0Vc+/N25Q== vscode-oniguruma@1.7.0: version "1.7.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index d62eb4b548b..2989156660d 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -53,15 +53,23 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-3.0.0.tgz#d52238c9052d746c9689523e650160e70786bc9a" integrity sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw== +"@vscode/deviceid@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@vscode/deviceid/-/deviceid-0.1.1.tgz#750e2930a3a8fbf3fd610096a8b915dfdb493c89" + integrity sha512-ErpoMeKKNYAkR1IT3zxB5RtiTqEECdh8fxggupWvzuxpTAX77hwOI2NdJ7um+vupnXRBZVx4ugo0+dVHJWUkag== + dependencies: + fs-extra "^11.2.0" + uuid "^9.0.1" + "@vscode/iconv-lite-umd@0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/proxy-agent@^0.19.0": - version "0.19.1" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.19.1.tgz#d9640d85df1c48885580b68bb4b2b54e17f5332c" - integrity sha512-cs1VOx6d5n69HhgzK0cWeyfudJt+9LdJi/vtgRRxxwisWKg4h83B3+EUJ4udF5SEkJgMBp3oU0jheZVt43ImnQ== +"@vscode/proxy-agent@^0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.22.0.tgz#bf571509d77c02c684be8c8526b7d0ac238c25cb" + integrity sha512-TQrv456pbrjmD6G+iOoXE1Mflm+8Ic/Kny4QU7ioiYe2+0HisvqzJM/CUa3Am5SWrNjMbntTHISjgmSaSlorrA== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -90,6 +98,11 @@ mkdirp "^1.0.4" node-addon-api "7.1.0" +"@vscode/tree-sitter-wasm@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.1.tgz#ffb2e295a416698f4c77cbffeca3b28567d6754b" + integrity sha512-m0GKnQ3BxWnVd+20KLGwr1+Qvt/RiiaJmKAqHNU35pNydDtduUzyBm7ETz/T0vOVKoeIAaiYsJOA1aKWs7Y1tA== + "@vscode/vscode-languagedetection@1.0.21": version "1.0.21" resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" @@ -114,45 +127,47 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.7.0-beta.12": - version "0.7.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.12.tgz#200f0293c507b75064963b5fc72a115799533920" - integrity sha512-euzQyWdklaSxzmb87kuwwiVP06vuYe1oUK+CiQW24UggSXThOEvZhvYV3O6iEgLe3p+7QfgnRWohXhCM84VOew== +"@xterm/addon-clipboard@0.2.0-beta.35": + version "0.2.0-beta.35" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.35.tgz#4c9b553ea63ce02a3c8075fea7df1637d52092ef" + integrity sha512-B8AulZEjsfvSEaLKp8oyRu7yJ7FJb5R3W0wpPbI/rOMVAuBwxDJsz0CxLvJUXnJX7OJwd5cjnyTnEcXJfMJycA== + dependencies: + js-base64 "^3.7.5" -"@xterm/addon-image@0.8.0-beta.12": - version "0.8.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.12.tgz#3fc5cea489d7159bf496b3a6d6515a109dab7226" - integrity sha512-YsBhmzwxRmym2dUA2CSm52Wt3OLhydVHM+SZmRAJ0/hvfB7dDjtuXBUSIdQWB16WWbGdi4Iazcs/TTxtarX/yA== +"@xterm/addon-image@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.52.tgz#a3115a50f884e5ba2f8ce09118a3d7e833fceb7b" + integrity sha512-1fWhnCIvLeO0aQ3CKqkTB9ye1bUsocpgFdDOgmwfW4XhLXpvu+QcyMGQMtWJHt8JWBN2w0cgR9eyfKw7orN+9Q== -"@xterm/addon-search@0.15.0-beta.12": - version "0.15.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.12.tgz#0c54677512135bbf820e3949c9dabeacc690d495" - integrity sha512-63ZhxXj6jBYumVrWJ7ZssICSMz+jHsXbi67tDQNMwTRO/MJxTittZeTHQ7IQrRYzKQgixrX0rLH7AwrLBrn2uQ== +"@xterm/addon-search@0.16.0-beta.52": + version "0.16.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.52.tgz#f8c77629b95ceff7d6b93d95c4b085857f405470" + integrity sha512-ZLVh0O91dcjxCjrU3vadl+40Z/mBnYXhKNA58oU/dGWFtFxtUB9SaZoOUtBvnfDpQIloYAK6raC2AfVsKHzD8A== -"@xterm/addon-serialize@0.13.0-beta.12": - version "0.13.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.12.tgz#438c1a6249cf4da3773d3de11a56638fa88d752a" - integrity sha512-/32Gpcj37Ftqf6b4+H62rcB70jLXi9IQspod/2mK3K+Yza9X+Yc8VkAz8VgpKa6tzbh3Xk0XEo/dB6kVFv1Jsg== +"@xterm/addon-serialize@0.14.0-beta.52": + version "0.14.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.52.tgz#19708cdd2895ddbd983b771ae9d14d7bc54cf7c9" + integrity sha512-1+ckKya1OURFmELH1Tjjoxz3Gnj78Dxj+NNRrEunfINkvyzaY+n8wT28FQxIlU5gJq+a0VGvlhNgTkMwgOn6aw== -"@xterm/addon-unicode11@0.8.0-beta.12": - version "0.8.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.12.tgz#7066297e2c662f6c235a4814e337c5a7fc8a91e2" - integrity sha512-uNsWmRpl4LaBfykpP9CKMo+49gVxRxHoC5MFuMhqPPNhXShsdBii3YxglwoKtit1fwzVT0CIWEniZQMlGiTIuw== +"@xterm/addon-unicode11@0.9.0-beta.52": + version "0.9.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.52.tgz#15afdf03c20d2e46b4eca68bd461eea71c8dd37f" + integrity sha512-5tZR/8c+vf0YNSYS6B9pEv8gyWWZpPYOf/BRQDkTGtYAnFf04MzggVE/U7tKUXGDzBhzwTPODq5qPNTX1xpGgw== -"@xterm/addon-webgl@0.18.0-beta.12": - version "0.18.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.12.tgz#410396fb6a3edd033eb16f334b88f31870952afb" - integrity sha512-wnIf5Xv0qAWQ0I1G5drKpEThA+D0f03iOTdtPR3uSLDfR8OsmpnSRgiR0Y0nAOnDmiCnDxu/wdBCKOAcXhWl2Q== +"@xterm/addon-webgl@0.19.0-beta.52": + version "0.19.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.52.tgz#695b20a5fa88ff92e115624149592080fad59594" + integrity sha512-kbPO9iR166xW8qgRkYmKX2Vu0kQHXpxYLQ9jY/01e5kvNrI/rqRDV63FIq14ncOi7N3+dmTuUkjvbg8anCpuIw== -"@xterm/headless@5.5.0-beta.12": - version "5.5.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.12.tgz#5dc79a98d4653c37c388144ea2ab4fab664304f3" - integrity sha512-s1AS30MYb0KJ7sEruyywAi79lAjSgjVOasb6EOgOalaQBYWf5BY2HKBU+GOyRPFkusgEIBg0f/ID8uS1fiku9A== +"@xterm/headless@5.6.0-beta.52": + version "5.6.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.52.tgz#7f934a7d7c5bbf88e2d78c22317cd2464bc9638a" + integrity sha512-f4QITQwotblRLW6YOHnK801wHJWfFDnjD7jUEwaaAXtSp32xH3jguWnMP9/uuQX45q9ndjDjnm1t5aXX7gwqBQ== -"@xterm/xterm@5.5.0-beta.12": - version "5.5.0-beta.12" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.12.tgz#a0f7445a10f958a4949fbe6989693c93e9e0265e" - integrity sha512-+I/vQh16ndYt8erj7zrxywPb+niyZC1W0H0w/ueDB3IPC7zPXxcETR0OGmglL7kq8Erb76ukBYXw9byXR2vtxg== +"@xterm/xterm@5.6.0-beta.52": + version "5.6.0-beta.52" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.52.tgz#ed7456a8b78ea1d00431737d078b5ab0bafbcdfe" + integrity sha512-aZh8IBdZPb2N4NjTt/fQ/C90z/PM3Zxkfoqhmlsp5+v3Otmyw5kd3DepeHK1SFW/pz0/xdj4KFgX8t8Y2lDRbA== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" @@ -161,6 +176,13 @@ agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: dependencies: debug "^4.3.4" +agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -183,11 +205,11 @@ bl@^4.0.3: readable-stream "^3.4.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" buffer-crc32@~0.2.3: version "0.2.13" @@ -260,10 +282,10 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -272,12 +294,21 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -graceful-fs@4.2.11: +graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -313,10 +344,13 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -ip@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" - integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" is-extglob@^2.1.1: version "2.1.1" @@ -335,19 +369,38 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== -kerberos@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.0.1.tgz#663b0b46883b4da84495f60f2e9e399a43a33ef5" - integrity sha512-O/jIgbdGK566eUhFwIcgalbqirYU/r76MW7/UFw06Fd9x5bSwgyZWL/Vm26aAmezQww/G9KYkmmJBkEkPk5HLw== +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jschardet@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.3.tgz#10c2289fdae91a0aa9de8bba9c59055fd78898d3" + integrity sha512-Q1PKVMK/uu+yjdlobgWIYkUOCR1SqUmW9m/eUJNNj4zI2N12i25v8fYpVf+zCakQeaTdBdhnZTFbVIAVZIVVOg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +kerberos@2.1.1-alpha.0: + version "2.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.1.1-alpha.0.tgz#c6d377b43c8206340fd184167754f2c81dad5ab1" + integrity sha512-II8N/ky/Vpd8y7LTxwCuAYoQ8XeV3HYxuK7IDmyoFacIhDljx4sdt/+sOwqgXEweQyCHlbZSKSaK82upqNM4Hw== dependencies: bindings "^1.5.0" - node-addon-api "^4.3.0" - prebuild-install "7.1.1" + node-addon-api "^6.1.0" + prebuild-install "^7.1.2" lru-cache@^6.0.0: version "6.0.0" @@ -421,20 +474,20 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-addon-api@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" - integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== -node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== -node-pty@1.1.0-beta11: - version "1.1.0-beta11" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta11.tgz#909d5dd8f9aa2a7857e7b632fd4d2d4768bdf69f" - integrity sha512-vTjF+VrvSCfPDILUkIT+YrG1Fdn06/eBRS2fc9a3JzYAvknMB1Ip8aoJhxl8hNpjWAbprmCEiV91mlfNpCD+GQ== +node-pty@1.1.0-beta19: + version "1.1.0-beta19" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta19.tgz#a74dc04429903c5ac49ee81a15a24590da67d4f3" + integrity sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A== dependencies: node-addon-api "^7.1.0" @@ -455,10 +508,10 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== +prebuild-install@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" @@ -537,22 +590,27 @@ smart-buffer@^4.2.0: integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== socks-proxy-agent@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz#ffc5859a66dac89b0c4dab90253b96705f3e7120" - integrity sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ== + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== dependencies: - agent-base "^7.0.1" + agent-base "^7.1.1" debug "^4.3.4" - socks "^2.7.1" + socks "^2.8.3" -socks@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -586,10 +644,10 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tas-client-umd@0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.8.tgz#38bd32d49545417a0ea67fb618e646298e1b67cc" - integrity sha512-0jAAujLmjjGXf9PzrNpjOrr/6CTpSOp8jX80NOHK5nlOTWWpwaZ16EOyrPdHnm2bVfPHvT0/RAD0xyiQHGQvCQ== +tas-client-umd@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.2.0.tgz#b71cc28f4c9b14f7b62f1ca4669886aa197e390c" + integrity sha512-oezN7mJVm5qZDVEby7OzxCLKUpUN5of0rY4dvOWaDF2JZBlGpd3BXceFN8B53qlTaIkVSzP65aAMT0Vc+/N25Q== to-regex-range@^5.0.1: version "5.0.1" @@ -605,11 +663,21 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + vscode-oniguruma@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" diff --git a/resources/linux/code.desktop b/resources/linux/code.desktop index e2e3f534751..4c939a2fab8 100755 --- a/resources/linux/code.desktop +++ b/resources/linux/code.desktop @@ -2,13 +2,13 @@ Name=@@NAME_LONG@@ Comment=Code Editing. Redefined. GenericName=Text Editor -Exec=@@EXEC@@ --unity-launch %F +Exec=@@EXEC@@ %F Icon=@@ICON@@ Type=Application StartupNotify=false StartupWMClass=@@NAME_SHORT@@ Categories=TextEditor;Development;IDE; -MimeType=text/plain;inode/directory;application/x-@@NAME@@-workspace; +MimeType=application/x-@@NAME@@-workspace; Actions=new-empty-window; Keywords=vscode; diff --git a/resources/linux/debian/postinst.template b/resources/linux/debian/postinst.template index 16acb1481bf..7dc5bef8c5c 100755 --- a/resources/linux/debian/postinst.template +++ b/resources/linux/debian/postinst.template @@ -35,20 +35,46 @@ if [ "@@NAME@@" != "code-oss" ]; then eval $(apt-config shell APT_TRUSTED_PARTS Dir::Etc::trustedparts/d) CODE_TRUSTED_PART=${APT_TRUSTED_PARTS}microsoft.gpg + RET=true + if [ -e '/usr/share/debconf/confmodule' ]; then + . /usr/share/debconf/confmodule + db_get @@NAME@@/add-microsoft-repo || true + fi + # Install repository source list WRITE_SOURCE=0 - if [ ! -f $CODE_SOURCE_PART ] && [ ! -f /etc/rpi-issue ]; then - # Write source list if it does not exist and we're not running on Raspberry Pi OS - WRITE_SOURCE=1 - elif grep -Eq "http:\/\/packages\.microsoft\.com\/repos\/vscode" $CODE_SOURCE_PART; then + if [ "$RET" = false ]; then + # The user does not want to add the microsoft repository + WRITE_SOURCE=0 + elif grep -q "http://packages.microsoft.com/repos/vscode" $CODE_SOURCE_PART; then # Migrate from old repository + WRITE_SOURCE=2 + elif grep -q "http://packages.microsoft.com/repos/code" $CODE_SOURCE_PART; then + # Migrate from old repository + WRITE_SOURCE=2 + elif apt-cache policy | grep -q "https://packages.microsoft.com/repos/code"; then + # Skip following checks if the repo is already known to apt + WRITE_SOURCE=0 + elif [ ! -f $CODE_SOURCE_PART ] && [ ! -f /etc/rpi-issue ]; then + # Write source list if it does not exist and we're not running on Raspberry Pi OS WRITE_SOURCE=1 elif grep -q "# disabled on upgrade to" $CODE_SOURCE_PART; then # Write source list if it was disabled by OS upgrade WRITE_SOURCE=1 fi - if [ "$WRITE_SOURCE" -eq "1" ]; then + if [ "$WRITE_SOURCE" -eq "1" ] && [ -e '/usr/share/debconf/confmodule' ]; then + # Ask the user whether to actually write the source list + db_input high @@NAME@@/add-microsoft-repo || true + db_go || true + + db_get @@NAME@@/add-microsoft-repo + if [ "$RET" = false ]; then + WRITE_SOURCE=0 + fi + fi + + if [ "$WRITE_SOURCE" -ne "0" ]; then echo "### THIS FILE IS AUTOMATICALLY CONFIGURED ### # You may comment out this entry, but any other modifications may be lost. deb [arch=amd64,arm64,armhf] https://packages.microsoft.com/repos/code stable main" > $CODE_SOURCE_PART diff --git a/resources/linux/debian/postrm.template b/resources/linux/debian/postrm.template index fb36d522f38..dcbfda95ea0 100755 --- a/resources/linux/debian/postrm.template +++ b/resources/linux/debian/postrm.template @@ -14,3 +14,22 @@ fi if hash update-mime-database 2>/dev/null; then update-mime-database /usr/share/mime fi + +RET=true +if [ -e '/usr/share/debconf/confmodule' ]; then + . /usr/share/debconf/confmodule + db_get @@NAME@@/add-microsoft-repo || true +fi +if [ "$RET" = "true" ]; then + eval $(apt-config shell APT_SOURCE_PARTS Dir::Etc::sourceparts/d) + CODE_SOURCE_PART=${APT_SOURCE_PARTS}vscode.list + rm -f $CODE_SOURCE_PART + + eval $(apt-config shell APT_TRUSTED_PARTS Dir::Etc::trustedparts/d) + CODE_TRUSTED_PART=${APT_TRUSTED_PARTS}microsoft.gpg + rm -f $CODE_TRUSTED_PART +fi + +if [ "$1" = "purge" ] && [ -e '/usr/share/debconf/confmodule' ]; then + db_purge +fi diff --git a/resources/linux/debian/templates.template b/resources/linux/debian/templates.template new file mode 100644 index 00000000000..7be5e039b26 --- /dev/null +++ b/resources/linux/debian/templates.template @@ -0,0 +1,6 @@ +Template: @@NAME@@/add-microsoft-repo +Type: boolean +Default: true +Description: Add Microsoft apt repository for Visual Studio Code? + The installer would like to add the Microsoft repository and signing + key to update VS Code through apt. diff --git a/resources/linux/snap/snapcraft.yaml b/resources/linux/snap/snapcraft.yaml index b7b93f4c59c..1d7412bdc71 100644 --- a/resources/linux/snap/snapcraft.yaml +++ b/resources/linux/snap/snapcraft.yaml @@ -30,15 +30,18 @@ parts: - libcurl3-gnutls - libcurl3-nss - libcurl4 + - libegl1 - libdrm2 - libgbm1 - libgl1 + - libgles2 - libglib2.0-0 - libgtk-3-0 - libibus-1.0-5 - libnss3 - libpango-1.0-0 - libsecret-1-0 + - libwayland-egl1 - libxcomposite1 - libxdamage1 - libxfixes3 diff --git a/resources/server/bin/helpers/browser.cmd b/resources/server/bin/helpers/browser.cmd index ffa6b1cf57d..59664f04afa 100644 --- a/resources/server/bin/helpers/browser.cmd +++ b/resources/server/bin/helpers/browser.cmd @@ -1,5 +1,5 @@ @echo off setlocal set ROOT_DIR=%~dp0..\.. -call "%ROOT_DIR%\node.exe" "%ROOT_DIR%\out\server-cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" "--openExternal" %* +start "Open Browser" /B "%ROOT_DIR%\node.exe" "%ROOT_DIR%\out\server-cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" "--openExternal" "%*" endlocal diff --git a/resources/server/bin/helpers/check-requirements-linux.sh b/resources/server/bin/helpers/check-requirements-linux.sh index 079557869e3..31a618fbd85 100644 --- a/resources/server/bin/helpers/check-requirements-linux.sh +++ b/resources/server/bin/helpers/check-requirements-linux.sh @@ -27,6 +27,7 @@ fi ARCH=$(uname -m) found_required_glibc=0 found_required_glibcxx=0 +MIN_GLIBCXX_VERSION="3.4.25" # Extract the ID value from /etc/os-release if [ -f /etc/os-release ]; then @@ -40,7 +41,10 @@ fi # Based on https://github.com/bminor/glibc/blob/520b1df08de68a3de328b65a25b86300a7ddf512/elf/cache.c#L162-L245 case $ARCH in x86_64) LDCONFIG_ARCH="x86-64";; - armv7l | armv8l) LDCONFIG_ARCH="hard-float";; + armv7l | armv8l) + MIN_GLIBCXX_VERSION="3.4.26" + LDCONFIG_ARCH="hard-float" + ;; arm64 | aarch64) BITNESS=$(getconf LONG_BIT) if [ "$BITNESS" = "32" ]; then @@ -81,7 +85,7 @@ if [ "$OS_ID" != "alpine" ]; then libstdcpp_real_path=$(readlink -f "$libstdcpp_path_line") libstdcpp_version=$(grep -ao 'GLIBCXX_[0-9]*\.[0-9]*\.[0-9]*' "$libstdcpp_real_path" | sort -V | tail -1) libstdcpp_version_number=$(echo "$libstdcpp_version" | sed 's/GLIBCXX_//') - if [ "$(printf '%s\n' "3.4.24" "$libstdcpp_version_number" | sort -V | head -n1)" = "3.4.24" ]; then + if [ "$(printf '%s\n' "$MIN_GLIBCXX_VERSION" "$libstdcpp_version_number" | sort -V | head -n1)" = "$MIN_GLIBCXX_VERSION" ]; then found_required_glibcxx=1 break fi @@ -92,7 +96,7 @@ else found_required_glibcxx=1 fi if [ "$found_required_glibcxx" = "0" ]; then - echo "Warning: Missing GLIBCXX >= 3.4.25! from $libstdcpp_real_path" + echo "Warning: Missing GLIBCXX >= $MIN_GLIBCXX_VERSION! from $libstdcpp_real_path" fi if [ "$OS_ID" = "alpine" ]; then diff --git a/resources/server/manifest.json b/resources/server/manifest.json index d92ca7ac018..443e3fd1cb1 100644 --- a/resources/server/manifest.json +++ b/resources/server/manifest.json @@ -3,9 +3,8 @@ "short_name": "Code- OSS", "start_url": "/", "lang": "en-US", - "display_override": [ - "window-controls-overlay" - ], + "display": "standalone", + "display_override": ["window-controls-overlay"], "icons": [ { "src": "code-192.png", diff --git a/scripts/code-server.js b/scripts/code-server.js index c043bf2671b..56945e76ca7 100644 --- a/scripts/code-server.js +++ b/scripts/code-server.js @@ -69,4 +69,3 @@ function startServer(programArgs) { } main(); - diff --git a/scripts/code.bat b/scripts/code.bat index 008c54fcbde..7f48b753559 100644 --- a/scripts/code.bat +++ b/scripts/code.bat @@ -23,9 +23,16 @@ set VSCODE_CLI=1 set ELECTRON_ENABLE_LOGGING=1 set ELECTRON_ENABLE_STACK_DUMPING=1 +set DISABLE_TEST_EXTENSION="--disable-extension=vscode.vscode-api-tests" +for %%A in (%*) do ( + if "%%~A"=="--extensionTestsPath" ( + set DISABLE_TEST_EXTENSION="" + ) +) + :: Launch Code -%CODE% . %* +%CODE% . %DISABLE_TEST_EXTENSION% %* goto end :builtin diff --git a/scripts/code.sh b/scripts/code.sh index 24929fdf351..c29b632cbcb 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -42,8 +42,13 @@ function code() { export ELECTRON_ENABLE_STACK_DUMPING=1 export ELECTRON_ENABLE_LOGGING=1 + DISABLE_TEST_EXTENSION="--disable-extension=vscode.vscode-api-tests" + if [[ "$@" == *"--extensionTestsPath"* ]]; then + DISABLE_TEST_EXTENSION="" + fi + # Launch Code - exec "$CODE" . "$@" + exec "$CODE" . $DISABLE_TEST_EXTENSION "$@" } function code-wsl() diff --git a/scripts/playground-server.ts b/scripts/playground-server.ts index 1c2074ee191..474f83def86 100644 --- a/scripts/playground-server.ts +++ b/scripts/playground-server.ts @@ -223,10 +223,10 @@ class DirWatcher { function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer, moduleIdMapper: SimpleModuleIdPathMapper): ChainableRequestHandler { return async (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); - const d = watcher.onDidChange(fsPath => { + const d = watcher.onDidChange((fsPath, newContent) => { const path = fileServer.filePathToUrlPath(fsPath); if (path) { - res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath) }) + '\n'); + res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath), newContent }) + '\n'); } }); res.on('close', () => d.dispose()); @@ -235,13 +235,15 @@ function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): string { loaderJsCode = loaderJsCode.replace( /constructor\(env, scriptLoader, defineFunc, requireFunc, loaderAvailableTimestamp = 0\) {/, - '$&globalThis.___globalModuleManager = this;' + '$&globalThis.___globalModuleManager = this; globalThis.vscode = { process: { env: { VSCODE_DEV: true } } }' ); const ___globalModuleManager: any = undefined; // This code will be appended to loader.js function $watchChanges(fileChangesUrl: string) { + interface HotReloadConfig { } + let reloadFn; if (globalThis.$sendMessageToParent) { reloadFn = () => globalThis.$sendMessageToParent({ kind: 'reload' }); @@ -262,49 +264,18 @@ function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): s buffer += new TextDecoder().decode(value); const lines = buffer.split('\n'); buffer = lines.pop()!; + + const changes: { relativePath: string; config: HotReloadConfig | undefined; path: string; newContent: string }[] = []; + for (const line of lines) { const data = JSON.parse(line); - let handled = false; - if (data.changedPath.endsWith('.css')) { - if (typeof document !== 'undefined') { - console.log('css changed', data.changedPath); - const styleSheet = [...document.querySelectorAll(`link[rel='stylesheet']`)].find((l: any) => new URL(l.href, document.location.href).pathname.endsWith(data.changedPath)) as any; - if (styleSheet) { - styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now(); - } - } - handled = true; - } else if (data.changedPath.endsWith('.js') && data.moduleId) { - console.log('js changed', data.changedPath); - const moduleId = ___globalModuleManager._moduleIdProvider.getModuleId(data.moduleId); - if (___globalModuleManager._modules2[moduleId]) { - const srcUrl = ___globalModuleManager._config.moduleIdToPaths(data.moduleId); - const newSrc = await (await fetch(srcUrl)).text(); - (new Function('define', newSrc))(function (deps, callback) { // CodeQL [SM01632] This code is only executed during development (as part of the dev-only playground-server). It is required for the hot-reload functionality. - const oldModule = ___globalModuleManager._modules2[moduleId]; - delete ___globalModuleManager._modules2[moduleId]; + const relativePath = data.changedPath.replace(/\\/g, '/').split('/out/')[1]; + changes.push({ config: {}, path: data.changedPath, relativePath, newContent: data.newContent }); + } - ___globalModuleManager.defineModule(data.moduleId, deps, callback); - const newModule = ___globalModuleManager._modules2[moduleId]; - const oldExports = { ...oldModule.exports }; - - Object.assign(oldModule.exports, newModule.exports); - newModule.exports = oldModule.exports; - - handled = true; - - for (const cb of [...globalThis.$hotReload_deprecateExports]) { - cb(oldExports, newModule.exports); - } - - if (handled) { - console.log('hot reloaded', data.moduleId); - } - }); - } - } - - if (!handled) { reloadFn(); } + const result = handleChanges(changes, 'playground-server'); + if (result.reloadFailedJsFiles.length > 0) { + reloadFn(); } } }).catch(err => { @@ -312,6 +283,163 @@ function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): s setTimeout(() => $watchChanges(fileChangesUrl), 1000); }); + + function handleChanges(changes: { + relativePath: string; + config: HotReloadConfig | undefined; + path: string; + newContent: string; + }[], debugSessionName: string) { + // This function is stringified and injected into the debuggee. + + const hotReloadData: { count: number; originalWindowTitle: any; timeout: any; shouldReload: boolean } = globalThis.$hotReloadData || (globalThis.$hotReloadData = { count: 0, messageHideTimeout: undefined, shouldReload: false }); + + const reloadFailedJsFiles: { relativePath: string; path: string }[] = []; + + for (const change of changes) { + handleChange(change.relativePath, change.path, change.newContent, change.config); + } + + return { reloadFailedJsFiles }; + + function handleChange(relativePath: string, path: string, newSrc: string, config: any) { + if (relativePath.endsWith('.css')) { + handleCssChange(relativePath); + } else if (relativePath.endsWith('.js')) { + handleJsChange(relativePath, path, newSrc, config); + } + } + + function handleCssChange(relativePath: string) { + if (typeof document === 'undefined') { + return; + } + + const styleSheet = (([...document.querySelectorAll(`link[rel='stylesheet']`)] as HTMLLinkElement[])) + .find(l => new URL(l.href, document.location.href).pathname.endsWith(relativePath)); + if (styleSheet) { + setMessage(`reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + console.log(debugSessionName, 'css reloaded', relativePath); + styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now(); + } else { + setMessage(`could not reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + console.log(debugSessionName, 'ignoring css change, as stylesheet is not loaded', relativePath); + } + } + + + function handleJsChange(relativePath: string, path: string, newSrc: string, config: any) { + const moduleIdStr = trimEnd(relativePath, '.js'); + + const requireFn: any = globalThis.require; + const moduleManager = (requireFn as any).moduleManager; + if (!moduleManager) { + console.log(debugSessionName, 'ignoring js change, as moduleManager is not available', relativePath); + return; + } + + const moduleId = moduleManager._moduleIdProvider.getModuleId(moduleIdStr); + const oldModule = moduleManager._modules2[moduleId]; + + if (!oldModule) { + console.log(debugSessionName, 'ignoring js change, as module is not loaded', relativePath); + return; + } + + // Check if we can reload + const g = globalThis as any; + + // A frozen copy of the previous exports + const oldExports = Object.freeze({ ...oldModule.exports }); + const reloadFn = g.$hotReload_applyNewExports?.({ oldExports, newSrc, config }); + + if (!reloadFn) { + console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath); + hotReloadData.shouldReload = true; + + reloadFailedJsFiles.push({ relativePath, path }); + + setMessage(`hot reload not supported for ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + return; + } + + // Eval maintains source maps + function newScript(/* this parameter is used by newSrc */ define) { + // eslint-disable-next-line no-eval + eval(newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality. + } + + newScript(/* define */ function (deps, callback) { + // Evaluating the new code was successful. + + // Redefine the module + delete moduleManager._modules2[moduleId]; + moduleManager.defineModule(moduleIdStr, deps, callback); + const newModule = moduleManager._modules2[moduleId]; + + + // Patch the exports of the old module, so that modules using the old module get the new exports + Object.assign(oldModule.exports, newModule.exports); + // We override the exports so that future reloads still patch the initial exports. + newModule.exports = oldModule.exports; + + const successful = reloadFn(newModule.exports); + if (!successful) { + hotReloadData.shouldReload = true; + setMessage(`hot reload failed ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + console.log(debugSessionName, 'hot reload was not successful', relativePath); + return; + } + + console.log(debugSessionName, 'hot reloaded', moduleIdStr); + setMessage(`successfully reloaded ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + }); + } + + function setMessage(message: string) { + const domElem = (document.querySelector('.titlebar-center .window-title')) as HTMLDivElement | undefined; + if (!domElem) { return; } + if (!hotReloadData.timeout) { + hotReloadData.originalWindowTitle = domElem.innerText; + } else { + clearTimeout(hotReloadData.timeout); + } + if (hotReloadData.shouldReload) { + message += ' (manual reload required)'; + } + + domElem.innerText = message; + hotReloadData.timeout = setTimeout(() => { + hotReloadData.timeout = undefined; + // If wanted, we can restore the previous title message + // domElem.replaceChildren(hotReloadData.originalWindowTitle); + }, 5000); + } + + function formatPath(path: string): string { + const parts = path.split('/'); + parts.reverse(); + let result = parts[0]; + parts.shift(); + for (const p of parts) { + if (result.length + p.length > 40) { + break; + } + result = p + '/' + result; + if (result.length > 20) { + break; + } + } + return result; + } + + function trimEnd(str, suffix) { + if (str.endsWith(suffix)) { + return str.substring(0, str.length - suffix.length); + } + return str; + } + } } const additionalJsCode = ` diff --git a/scripts/xterm-update.js b/scripts/xterm-update.js index 747d7af3ea8..8ede619160b 100644 --- a/scripts/xterm-update.js +++ b/scripts/xterm-update.js @@ -8,7 +8,7 @@ const path = require('path'); const moduleNames = [ '@xterm/xterm', - '@xterm/addon-canvas', + '@xterm/addon-clipboard', '@xterm/addon-image', '@xterm/addon-search', '@xterm/addon-serialize', diff --git a/scripts/xterm-update.ps1 b/scripts/xterm-update.ps1 index 11c282de888..2eae7fffe77 100644 --- a/scripts/xterm-update.ps1 +++ b/scripts/xterm-update.ps1 @@ -1 +1,2 @@ -node $PSScriptRoot\xterm-update.js (Get-Location) +$scriptPath = Join-Path $PSScriptRoot "xterm-update.js" +node $scriptPath (Get-Location) diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index cc47b050fb5..c228faccfed 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -6,6 +6,48 @@ //@ts-check 'use strict'; +/** + * @import { INLSConfiguration } from './vs/nls' + * @import { IProductConfiguration } from './vs/base/common/product' + */ + +// ESM-comment-begin +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// import * as path from 'path'; +// import * as fs from 'fs'; +// import { fileURLToPath } from 'url'; +// import { createRequire, register } from 'node:module'; +// import { product, pkg } from './bootstrap-meta.js'; +// import * as bootstrapNode from './bootstrap-node.js'; +// import * as performance from './vs/base/common/performance.js'; +// +// const require = createRequire(import.meta.url); +// const isESM = true; +// const module = { exports: {} }; +// const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// +// // Install a hook to module resolution to map 'fs' to 'original-fs' +// if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { +// const jsCode = ` +// export async function resolve(specifier, context, nextResolve) { +// if (specifier === 'fs') { +// return { +// format: 'builtin', +// shortCircuit: true, +// url: 'node:original-fs' +// }; +// } + +// // Defer to the next hook in the chain, which would be the +// // Node.js default resolve if this is the last user-specified loader. +// return nextResolve(specifier, context); +// }`; +// register(`data:text/javascript;base64,${Buffer.from(jsCode).toString('base64')}`, import.meta.url); +// } +// ESM-uncomment-end + // Store the node.js require function in a variable // before loading our AMD loader to avoid issues // when this file is bundled with other files. @@ -15,8 +57,13 @@ const nodeRequire = require; globalThis._VSCODE_NODE_MODULES = new Proxy(Object.create(null), { get: (_target, mod) => nodeRequire(String(mod)) }); // VSCODE_GLOBALS: package/product.json -/** @type Record */ -globalThis._VSCODE_PRODUCT_JSON = require('../product.json'); +/** @type Partial */ +// ESM-comment-begin +globalThis._VSCODE_PRODUCT_JSON = require('./bootstrap-meta').product; +// ESM-comment-end +// ESM-uncomment-begin +// globalThis._VSCODE_PRODUCT_JSON = { ...product }; +// ESM-uncomment-end if (process.env['VSCODE_DEV']) { // Patch product overrides when running out of sources try { @@ -25,63 +72,179 @@ if (process.env['VSCODE_DEV']) { globalThis._VSCODE_PRODUCT_JSON = Object.assign(globalThis._VSCODE_PRODUCT_JSON, overrides); } catch (error) { /* ignore */ } } -globalThis._VSCODE_PACKAGE_JSON = require('../package.json'); +// ESM-comment-begin +globalThis._VSCODE_PACKAGE_JSON = require('./bootstrap-meta').pkg; +// ESM-comment-end +// ESM-uncomment-begin +// globalThis._VSCODE_PACKAGE_JSON = { ...pkg }; +// ESM-uncomment-end -// @ts-ignore -const loader = require('./vs/loader'); -const bootstrap = require('./bootstrap'); -const performance = require('./vs/base/common/performance'); +// VSCODE_GLOBALS: file root of all resources +globalThis._VSCODE_FILE_ROOT = __dirname; -// Bootstrap: NLS -const nlsConfig = bootstrap.setupNLS(); +// ESM-comment-begin +const bootstrapNode = require('./bootstrap-node'); +const performance = require(`./vs/base/common/performance`); +const fs = require('fs'); +// ESM-comment-end -// Bootstrap: Loader -loader.config({ - baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), - catchError: true, - nodeRequire, - 'vs/nls': nlsConfig, - amdModulesPattern: /^vs\//, - recordStats: true -}); +//#region NLS helpers -// Running in Electron -if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { - loader.define('fs', ['original-fs'], function (/** @type {import('fs')} */originalFS) { - return originalFS; // replace the patched electron fs with the original node fs for all AMD code - }); -} +/** @type {Promise | undefined} */ +let setupNLSResult = undefined; -// Pseudo NLS support -if (nlsConfig && nlsConfig.pseudo) { - loader(['vs/nls'], function (/** @type {import('vs/nls')} */nlsPlugin) { - nlsPlugin.setPseudoTranslation(!!nlsConfig.pseudo); - }); +/** + * @returns {Promise} + */ +function setupNLS() { + if (!setupNLSResult) { + setupNLSResult = doSetupNLS(); + } + + return setupNLSResult; } /** - * @param {string=} entrypoint - * @param {(value: any) => void=} onLoad - * @param {(err: Error) => void=} onError + * @returns {Promise} */ -exports.load = function (entrypoint, onLoad, onError) { - if (!entrypoint) { - return; +async function doSetupNLS() { + performance.mark('code/amd/willLoadNls'); + + /** @type {INLSConfiguration | undefined} */ + let nlsConfig = undefined; + + /** @type {string | undefined} */ + let messagesFile; + if (process.env['VSCODE_NLS_CONFIG']) { + try { + /** @type {INLSConfiguration} */ + nlsConfig = JSON.parse(process.env['VSCODE_NLS_CONFIG']); + if (nlsConfig?.languagePack?.messagesFile) { + messagesFile = nlsConfig.languagePack.messagesFile; + } else if (nlsConfig?.defaultMessagesFile) { + messagesFile = nlsConfig.defaultMessagesFile; + } + + globalThis._VSCODE_NLS_LANGUAGE = nlsConfig?.resolvedLanguage; + } catch (e) { + console.error(`Error reading VSCODE_NLS_CONFIG from environment: ${e}`); + } } - // code cache config - if (process.env['VSCODE_CODE_CACHE_PATH']) { - loader.config({ - nodeCachedData: { - path: process.env['VSCODE_CODE_CACHE_PATH'], - seed: entrypoint + if ( + process.env['VSCODE_DEV'] || // no NLS support in dev mode + !messagesFile // no NLS messages file + ) { + return undefined; + } + + try { + globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(messagesFile)).toString()); + } catch (error) { + console.error(`Error reading NLS messages file ${messagesFile}: ${error}`); + + // Mark as corrupt: this will re-create the language pack cache next startup + if (nlsConfig?.languagePack?.corruptMarkerFile) { + try { + await fs.promises.writeFile(nlsConfig.languagePack.corruptMarkerFile, 'corrupted'); + } catch (error) { + console.error(`Error writing corrupted NLS marker file: ${error}`); } + } + + // Fallback to the default message file to ensure english translation at least + if (nlsConfig?.defaultMessagesFile && nlsConfig.defaultMessagesFile !== messagesFile) { + try { + globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(nlsConfig.defaultMessagesFile)).toString()); + } catch (error) { + console.error(`Error reading default NLS messages file ${nlsConfig.defaultMessagesFile}: ${error}`); + } + } + } + + performance.mark('code/amd/didLoadNls'); + + return nlsConfig; +} + +//#endregion + +//#region Loader Config + +if (isESM) { + + /** + * @param {string=} entrypoint + * @param {(value: any) => void} [onLoad] + * @param {(err: Error) => void} [onError] + */ + module.exports.load = function (entrypoint, onLoad, onError) { + if (!entrypoint) { + return; + } + + entrypoint = `./${entrypoint}.js`; + + onLoad = onLoad || function () { }; + onError = onError || function (err) { console.error(err); }; + + setupNLS().then(() => { + performance.mark(`code/fork/willLoadCode`); + import(entrypoint).then(onLoad, onError); + }); + }; +} else { + + // @ts-ignore + const loader = require('./vs/loader'); + + loader.config({ + baseUrl: bootstrapNode.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), + catchError: true, + nodeRequire, + amdModulesPattern: /^vs\//, + recordStats: true + }); + + // Running in Electron + if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { + loader.define('fs', ['original-fs'], function (/** @type {import('fs')} */originalFS) { + return originalFS; // replace the patched electron fs with the original node fs for all AMD code }); } - onLoad = onLoad || function () { }; - onError = onError || function (err) { console.error(err); }; + /** + * @param {string=} entrypoint + * @param {(value: any) => void} [onLoad] + * @param {(err: Error) => void} [onError] + */ + module.exports.load = function (entrypoint, onLoad, onError) { + if (!entrypoint) { + return; + } - performance.mark('code/fork/willLoadCode'); - loader([entrypoint], onLoad, onError); -}; + // code cache config + if (process.env['VSCODE_CODE_CACHE_PATH']) { + loader.config({ + nodeCachedData: { + path: process.env['VSCODE_CODE_CACHE_PATH'], + seed: entrypoint + } + }); + } + + onLoad = onLoad || function () { }; + onError = onError || function (err) { console.error(err); }; + + setupNLS().then(() => { + performance.mark('code/fork/willLoadCode'); + loader([entrypoint], onLoad, onError); + }); + }; +} + +//#endregion + +// ESM-uncomment-begin +// export const load = module.exports.load; +// ESM-uncomment-end diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 9de1e6f0d15..009fe3e7e2d 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -6,11 +6,18 @@ //@ts-check 'use strict'; +// ESM-comment-begin const performance = require('./vs/base/common/performance'); -performance.mark('code/fork/start'); - -const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); +const bootstrapAmd = require('./bootstrap-amd'); +// ESM-comment-end +// ESM-uncomment-begin +// import * as performance from './vs/base/common/performance.js'; +// import * as bootstrapNode from './bootstrap-node.js'; +// import * as bootstrapAmd from './bootstrap-amd.js'; +// ESM-uncomment-end + +performance.mark('code/fork/start'); // Crash reporter configureCrashReporter(); @@ -19,7 +26,7 @@ configureCrashReporter(); bootstrapNode.removeGlobalNodeModuleLookupPaths(); // Enable ASAR in our forked processes -bootstrap.enableASARSupport(); +bootstrapNode.enableASARSupport(); if (process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']) { bootstrapNode.injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); @@ -41,7 +48,7 @@ if (process.env['VSCODE_PARENT_PID']) { } // Load AMD entry point -require('./bootstrap-amd').load(process.env['VSCODE_AMD_ENTRYPOINT']); +bootstrapAmd.load(process.env['VSCODE_AMD_ENTRYPOINT']); //#region Helpers diff --git a/src/bootstrap-meta.js b/src/bootstrap-meta.js new file mode 100644 index 00000000000..5415bc5df6a --- /dev/null +++ b/src/bootstrap-meta.js @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +'use strict'; + +/** + * @import { IProductConfiguration } from './vs/base/common/product' + */ + +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// +// const require = createRequire(import.meta.url); +// const module = { exports: {} }; +// ESM-uncomment-end + +/** @type Partial & { BUILD_INSERT_PRODUCT_CONFIGURATION?: string } */ +let productObj = { BUILD_INSERT_PRODUCT_CONFIGURATION: 'BUILD_INSERT_PRODUCT_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (productObj['BUILD_INSERT_PRODUCT_CONFIGURATION']) { + // @ts-ignore + productObj = require('../product.json'); // Running out of sources +} + +/** @type object & { BUILD_INSERT_PACKAGE_CONFIGURATION?: string } */ +let pkgObj = { BUILD_INSERT_PACKAGE_CONFIGURATION: 'BUILD_INSERT_PACKAGE_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (pkgObj['BUILD_INSERT_PACKAGE_CONFIGURATION']) { + // @ts-ignore + pkgObj = require('../package.json'); // Running out of sources +} + +module.exports.product = productObj; +module.exports.pkg = pkgObj; + +// ESM-uncomment-begin +// export const product = module.exports.product; +// export const pkg = module.exports.pkg; +// ESM-uncomment-end diff --git a/src/bootstrap-node.js b/src/bootstrap-node.js index 914b8290380..5183526e1d5 100644 --- a/src/bootstrap-node.js +++ b/src/bootstrap-node.js @@ -6,12 +6,48 @@ //@ts-check 'use strict'; +// ESM-comment-begin +const path = require('path'); +const fs = require('fs'); +const Module = require('module'); + +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// import * as path from 'path'; +// import * as fs from 'fs'; +// import { fileURLToPath } from 'url'; +// import { createRequire } from 'node:module'; +// +// const require = createRequire(import.meta.url); +// const Module = require('module'); +// const isESM = true; +// const module = { exports: {} }; +// const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// ESM-uncomment-end + +// increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) +Error.stackTraceLimit = 100; + +if (!process.env['VSCODE_HANDLES_SIGPIPE']) { + // Workaround for Electron not installing a handler to ignore SIGPIPE + // (https://github.com/electron/electron/issues/13254) + let didLogAboutSIGPIPE = false; + process.on('SIGPIPE', () => { + // See https://github.com/microsoft/vscode-remote-release/issues/6543 + // In certain situations, the console itself can be in a broken pipe state + // so logging SIGPIPE to the console will cause an infinite async loop + if (!didLogAboutSIGPIPE) { + didLogAboutSIGPIPE = true; + console.error(new Error(`Unexpected SIGPIPE`)); + } + }); +} + // Setup current working directory in all our node & electron processes // - Windows: call `process.chdir()` to always set application folder as cwd // - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups function setupCurrentWorkingDirectory() { - const path = require('path'); - try { // Store the `process.cwd()` inside `VSCODE_CWD` @@ -38,36 +74,41 @@ setupCurrentWorkingDirectory(); * * @param {string} injectPath */ -exports.injectNodeModuleLookupPath = function (injectPath) { +module.exports.injectNodeModuleLookupPath = function (injectPath) { if (!injectPath) { throw new Error('Missing injectPath'); } - const Module = require('module'); - const path = require('path'); + const Module = require('node:module'); + if (isESM) { + // register a loader hook + // ESM-uncomment-begin + // Module.register('./loader-lookup-path.mjs', { parentURL: import.meta.url, data: injectPath }); + // ESM-uncomment-end + } else { + const nodeModulesPath = path.join(__dirname, '../node_modules'); - const nodeModulesPath = path.join(__dirname, '../node_modules'); + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; - // @ts-ignore - const originalResolveLookupPaths = Module._resolveLookupPaths; - - // @ts-ignore - Module._resolveLookupPaths = function (moduleName, parent) { - const paths = originalResolveLookupPaths(moduleName, parent); - if (Array.isArray(paths)) { - for (let i = 0, len = paths.length; i < len; i++) { - if (paths[i] === nodeModulesPath) { - paths.splice(i, 0, injectPath); - break; + // @ts-ignore + Module._resolveLookupPaths = function (moduleName, parent) { + const paths = originalResolveLookupPaths(moduleName, parent); + if (Array.isArray(paths)) { + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === nodeModulesPath) { + paths.splice(i, 0, injectPath); + break; + } } } - } - return paths; - }; + return paths; + }; + } }; -exports.removeGlobalNodeModuleLookupPaths = function () { +module.exports.removeGlobalNodeModuleLookupPaths = function () { const Module = require('module'); // @ts-ignore const globalPaths = Module.globalPaths; @@ -95,10 +136,7 @@ exports.removeGlobalNodeModuleLookupPaths = function () { * @param {Partial} product * @returns {{ portableDataPath: string; isPortable: boolean; }} */ -exports.configurePortable = function (product) { - const fs = require('fs'); - const path = require('path'); - +module.exports.configurePortable = function (product) { const appRoot = path.dirname(__dirname); /** @@ -158,3 +196,75 @@ exports.configurePortable = function (product) { isPortable }; }; + +/** + * Helper to enable ASAR support. + */ +module.exports.enableASARSupport = function () { + const NODE_MODULES_PATH = path.join(__dirname, '../node_modules'); + const NODE_MODULES_ASAR_PATH = `${NODE_MODULES_PATH}.asar`; + + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; + + // @ts-ignore + Module._resolveLookupPaths = function (request, parent) { + const paths = originalResolveLookupPaths(request, parent); + if (Array.isArray(paths)) { + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === NODE_MODULES_PATH) { + paths.splice(i, 0, NODE_MODULES_ASAR_PATH); + break; + } + } + } + + return paths; + }; +}; + +/** + * Helper to convert a file path to a URI. + * + * TODO@bpasero check for removal once ESM has landed. + * + * @param {string} path + * @param {{ isWindows?: boolean, scheme?: string, fallbackAuthority?: string }} config + * @returns {string} + */ +module.exports.fileUriFromPath = function (path, config) { + + // Since we are building a URI, we normalize any backslash + // to slashes and we ensure that the path begins with a '/'. + let pathName = path.replace(/\\/g, '/'); + if (pathName.length > 0 && pathName.charAt(0) !== '/') { + pathName = `/${pathName}`; + } + + /** @type {string} */ + let uri; + + // Windows: in order to support UNC paths (which start with '//') + // that have their own authority, we do not use the provided authority + // but rather preserve it. + if (config.isWindows && pathName.startsWith('//')) { + uri = encodeURI(`${config.scheme || 'file'}:${pathName}`); + } + + // Otherwise we optionally add the provided authority if specified + else { + uri = encodeURI(`${config.scheme || 'file'}://${config.fallbackAuthority || ''}${pathName}`); + } + + return uri.replace(/#/g, '%23'); +}; + +//#endregion + +// ESM-uncomment-begin +// export const injectNodeModuleLookupPath = module.exports.injectNodeModuleLookupPath; +// export const removeGlobalNodeModuleLookupPaths = module.exports.removeGlobalNodeModuleLookupPaths; +// export const configurePortable = module.exports.configurePortable; +// export const enableASARSupport = module.exports.enableASARSupport; +// export const fileUriFromPath = module.exports.fileUriFromPath; +// ESM-uncomment-end diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 85409cc5a21..a747a7cf5dc 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -8,29 +8,31 @@ //@ts-check 'use strict'; +/** + * @import { ISandboxConfiguration } from './vs/base/parts/sandbox/common/sandboxTypes' + * @typedef {any} LoaderConfig + */ + /* eslint-disable no-restricted-globals */ -// Simple module style to support node.js and browser environments -(function (globalThis, factory) { +// ESM-comment-begin +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// const isESM = true; +// ESM-uncomment-end - // Node.js - if (typeof exports === 'object') { - module.exports = factory(); - } - - // Browser - else { - // @ts-ignore - globalThis.MonacoBootstrapWindow = factory(); - } -}(this, function () { - const bootstrapLib = bootstrap(); +(function (factory) { + // @ts-ignore + globalThis.MonacoBootstrapWindow = factory(); +}(function () { const preloadGlobals = sandboxGlobals(); const safeProcess = preloadGlobals.process; + // increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) + Error.stackTraceLimit = 100; + /** - * @typedef {import('./vs/base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @param {string[]} modulePaths * @param {(result: unknown, configuration: ISandboxConfiguration) => Promise | undefined} resultCallback * @param {{ @@ -80,85 +82,151 @@ developerDeveloperKeybindingsDisposable = registerDeveloperKeybindings(disallowReloadKeybinding); } - // Get the nls configuration into the process.env as early as possible - // @ts-ignore - const nlsConfig = globalThis.MonacoBootstrap.setupNLS(); - - let locale = nlsConfig.availableLanguages['*'] || 'en'; - if (locale === 'zh-tw') { - locale = 'zh-Hant'; - } else if (locale === 'zh-cn') { - locale = 'zh-Hans'; + globalThis._VSCODE_NLS_MESSAGES = configuration.nls.messages; + globalThis._VSCODE_NLS_LANGUAGE = configuration.nls.language; + let language = configuration.nls.language || 'en'; + if (language === 'zh-tw') { + language = 'zh-Hant'; + } else if (language === 'zh-cn') { + language = 'zh-Hans'; } - window.document.documentElement.setAttribute('lang', locale); + window.document.documentElement.setAttribute('lang', language); window['MonacoEnvironment'] = {}; - /** - * @typedef {any} LoaderConfig - */ - /** @type {LoaderConfig} */ - const loaderConfig = { - baseUrl: `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32', scheme: 'vscode-file', fallbackAuthority: 'vscode-app' })}/out`, - 'vs/nls': nlsConfig, - preferScriptTags: true - }; + if (isESM) { - // use a trusted types policy when loading via script tags - loaderConfig.trustedTypesPolicy = window.trustedTypes?.createPolicy('amdLoader', { - createScriptURL(value) { - if (value.startsWith(window.location.origin)) { - return value; - } - throw new Error(`Invalid script url: ${value}`); + // Signal before require() + if (typeof options?.beforeRequire === 'function') { + options.beforeRequire(configuration); } - }); - // Teach the loader the location of the node modules we use in renderers - // This will enable to load these modules via - + + + + diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 578b00a796e..d2ade1caf0b 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -10,13 +10,12 @@ import { hostname, release } from 'os'; import { VSBuffer } from 'vs/base/common/buffer'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; -import { isEqualOrParent } from 'vs/base/common/extpath'; import { Event } from 'vs/base/common/event'; -import { stripComments } from 'vs/base/common/json'; +import { parse } from 'vs/base/common/jsonc'; import { getPathLabel } from 'vs/base/common/labels'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas, VSCODE_AUTHORITY } from 'vs/base/common/network'; -import { isAbsolute, join, posix } from 'vs/base/common/path'; +import { join, posix } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from 'vs/base/common/platform'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -26,7 +25,7 @@ import { getDelayedChannel, ProxyChannel, StaticRouter } from 'vs/base/parts/ipc import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron'; import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp'; import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net'; -import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; +import { IProxyAuthService, ProxyAuthService } from 'vs/platform/native/electron-main/auth'; import { localize } from 'vs/nls'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; @@ -52,8 +51,9 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IIssueMainService } from 'vs/platform/issue/common/issue'; +import { IProcessMainService, IIssueMainService } from 'vs/platform/issue/common/issue'; import { IssueMainService } from 'vs/platform/issue/electron-main/issueMainService'; +import { ProcessMainService } from 'vs/platform/issue/electron-main/processMainService'; import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService'; import { ILaunchMainService, LaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -105,7 +105,7 @@ import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/e import { UserDataProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataProfilesHandler'; import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc'; import { Promises, RunOnceScheduler, runWhenGlobalIdle } from 'vs/base/common/async'; -import { resolveMachineId, resolveSqmId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolvedevDeviceId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LoggerChannel } from 'vs/platform/log/electron-main/logIpc'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; @@ -121,7 +121,6 @@ import { Lazy } from 'vs/base/common/lazy'; import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { AuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService'; import { normalizeNFC } from 'vs/base/common/normalization'; - /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). @@ -366,7 +365,7 @@ export class CodeApplication extends Disposable { process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason)); // Dispose on shutdown - this.lifecycleMainService.onWillShutdown(() => this.dispose()); + Event.once(this.lifecycleMainService.onWillShutdown)(() => this.dispose()); // Contextmenu via IPC support registerContextMenuListener(); @@ -496,31 +495,13 @@ export class CodeApplication extends Disposable { return this.resolveShellEnvironment(args, env, false); }); - validatedIpcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { - const uri = this.validateNlsPath([path]); - if (!uri || typeof data !== 'string') { - throw new Error('Invalid operation (vscode:writeNlsFile)'); - } - - return this.fileService.writeFile(uri, VSBuffer.fromString(data)); - }); - - validatedIpcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { - const uri = this.validateNlsPath(paths); - if (!uri) { - throw new Error('Invalid operation (vscode:readNlsFile)'); - } - - return (await this.fileService.readFile(uri)).value.toString(); - }); - validatedIpcMain.on('vscode:toggleDevTools', event => event.sender.toggleDevTools()); validatedIpcMain.on('vscode:openDevTools', event => event.sender.openDevTools()); validatedIpcMain.on('vscode:reloadWindow', event => event.sender.reload()); validatedIpcMain.handle('vscode:notifyZoomLevel', async (event, zoomLevel: number | undefined) => { - const window = this.windowsMainService?.getWindowById(event.sender.id); + const window = this.windowsMainService?.getWindowByWebContents(event.sender); if (window) { window.notifyZoomLevel(zoomLevel); } @@ -529,26 +510,6 @@ export class CodeApplication extends Disposable { //#endregion } - private validateNlsPath(pathSegments: unknown[]): URI | undefined { - let path: string | undefined = undefined; - - for (const pathSegment of pathSegments) { - if (typeof pathSegment === 'string') { - if (typeof path !== 'string') { - path = pathSegment; - } else { - path = join(path, pathSegment); - } - } - } - - if (typeof path !== 'string' || !isAbsolute(path) || !isEqualOrParent(path, this.environmentMainService.cachedLanguagesPath, !isLinux)) { - return undefined; - } - - return URI.file(path); - } - private onUnexpectedError(error: Error): void { if (error) { @@ -598,7 +559,7 @@ export class CodeApplication extends Disposable { // Main process server (electron IPC based) const mainProcessElectronServer = new ElectronIPCServer(); - this.lifecycleMainService.onWillShutdown(e => { + Event.once(this.lifecycleMainService.onWillShutdown)(e => { if (e.reason === ShutdownReason.KILL) { // When we go down abnormally, make sure to free up // any IPC we accept from other windows to reduce @@ -611,20 +572,21 @@ export class CodeApplication extends Disposable { // Resolve unique machine ID this.logService.trace('Resolving machine identifier...'); - const [machineId, sqmId] = await Promise.all([ + const [machineId, sqmId, devDeviceId] = await Promise.all([ resolveMachineId(this.stateService, this.logService), - resolveSqmId(this.stateService, this.logService) + resolveSqmId(this.stateService, this.logService), + resolvedevDeviceId(this.stateService, this.logService) ]); this.logService.trace(`Resolved machine identifier: ${machineId}`); // Shared process - const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId); + const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId, devDeviceId); // Services - const appInstantiationService = await this.initServices(machineId, sqmId, sharedProcessReady); + const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady); // Auth Handler - this._register(appInstantiationService.createInstance(ProxyAuthHandler)); + appInstantiationService.invokeFunction(accessor => accessor.get(IProxyAuthService)); // Transient profiles handler this._register(appInstantiationService.createInstance(UserDataProfilesHandler)); @@ -864,34 +826,38 @@ export class CodeApplication extends Disposable { // To: vscode-remote://wsl+ubuntu/mnt/c/GitDevelopment/monaco const secondSlash = uri.path.indexOf(posix.sep, 1 /* skip over the leading slash */); + let authority: string; + let path: string; if (secondSlash !== -1) { - const authority = uri.path.substring(1, secondSlash); - const path = uri.path.substring(secondSlash); - - let query = uri.query; - const params = new URLSearchParams(uri.query); - if (params.get('windowId') === '_blank') { - // Make sure to unset any `windowId=_blank` here - // https://github.com/microsoft/vscode/issues/191902 - params.delete('windowId'); - query = params.toString(); - } - - const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query, fragment: uri.fragment }); - - if (hasWorkspaceFileExtension(path)) { - return { workspaceUri: remoteUri }; - } - - if (/:[\d]+$/.test(path)) { - // path with :line:column syntax - return { fileUri: remoteUri }; - } - - return { folderUri: remoteUri }; + authority = uri.path.substring(1, secondSlash); + path = uri.path.substring(secondSlash); + } else { + authority = uri.path.substring(1); + path = '/'; } - } + let query = uri.query; + const params = new URLSearchParams(uri.query); + if (params.get('windowId') === '_blank') { + // Make sure to unset any `windowId=_blank` here + // https://github.com/microsoft/vscode/issues/191902 + params.delete('windowId'); + query = params.toString(); + } + + const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query, fragment: uri.fragment }); + + if (hasWorkspaceFileExtension(path)) { + return { workspaceUri: remoteUri }; + } + + if (/:[\d]+$/.test(path)) { + // path with :line:column syntax + return { fileUri: remoteUri }; + } + + return { folderUri: remoteUri }; + } return undefined; } @@ -986,8 +952,10 @@ export class CodeApplication extends Disposable { return false; } - private setupSharedProcess(machineId: string, sqmId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { - const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId)); + private setupSharedProcess(machineId: string, sqmId: string, devDeviceId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { + const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId, devDeviceId)); + + this._register(sharedProcess.onDidCrash(() => this.windowsMainService?.sendToFocused('vscode:reportSharedProcessCrash'))); const sharedProcessClient = (async () => { this.logService.trace('Main->SharedProcess#connect'); @@ -1008,7 +976,7 @@ export class CodeApplication extends Disposable { return { sharedProcessReady, sharedProcessClient }; } - private async initServices(machineId: string, sqmId: string, sharedProcessReady: Promise): Promise { + private async initServices(machineId: string, sqmId: string, devDeviceId: string, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); // Update @@ -1031,7 +999,7 @@ export class CodeApplication extends Disposable { } // Windows - services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, this.userEnv], false)); + services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, devDeviceId, this.userEnv], false)); services.set(IAuxiliaryWindowsMainService, new SyncDescriptor(AuxiliaryWindowsMainService, undefined, false)); // Dialogs @@ -1048,6 +1016,9 @@ export class CodeApplication extends Disposable { // Issues services.set(IIssueMainService, new SyncDescriptor(IssueMainService, [this.userEnv])); + // Process + services.set(IProcessMainService, new SyncDescriptor(ProcessMainService, [this.userEnv])); + // Encryption services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService)); @@ -1111,7 +1082,7 @@ export class CodeApplication extends Disposable { const isInternal = isInternalTelemetry(this.productService, this.configurationService); const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, isInternal); + const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, devDeviceId, isInternal); const piiPaths = getPiiPathsFromEnvironment(this.environmentMainService); const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true }; @@ -1127,6 +1098,9 @@ export class CodeApplication extends Disposable { // Utility Process Worker services.set(IUtilityProcessWorkerMainService, new SyncDescriptor(UtilityProcessWorkerMainService, undefined, true)); + // Proxy Auth + services.set(IProxyAuthService, new SyncDescriptor(ProxyAuthService)); + // Init services that require it await Promises.settled([ backupMainService.initialize(), @@ -1152,14 +1126,14 @@ export class CodeApplication extends Disposable { this.mainProcessNodeIpcServer.registerChannel('diagnostics', diagnosticsChannel); // Policies (main & shared process) - const policyChannel = new PolicyChannel(accessor.get(IPolicyService)); + const policyChannel = disposables.add(new PolicyChannel(accessor.get(IPolicyService))); mainProcessElectronServer.registerChannel('policy', policyChannel); sharedProcessClient.then(client => client.registerChannel('policy', policyChannel)); // Local Files const diskFileSystemProvider = this.fileService.getProvider(Schemas.file); assertType(diskFileSystemProvider instanceof DiskFileSystemProvider); - const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService, this.environmentMainService); + const fileSystemProviderChannel = disposables.add(new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService, this.environmentMainService)); mainProcessElectronServer.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel); sharedProcessClient.then(client => client.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel)); @@ -1180,6 +1154,10 @@ export class CodeApplication extends Disposable { const issueChannel = ProxyChannel.fromService(accessor.get(IIssueMainService), disposables); mainProcessElectronServer.registerChannel('issue', issueChannel); + // Process + const processChannel = ProxyChannel.fromService(accessor.get(IProcessMainService), disposables); + mainProcessElectronServer.registerChannel('process', processChannel); + // Encryption const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService), disposables); mainProcessElectronServer.registerChannel('encryption', encryptionChannel); @@ -1215,12 +1193,12 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('webview', webviewChannel); // Storage (main & shared process) - const storageChannel = this._register(new StorageDatabaseChannel(this.logService, accessor.get(IStorageMainService))); + const storageChannel = disposables.add((new StorageDatabaseChannel(this.logService, accessor.get(IStorageMainService)))); mainProcessElectronServer.registerChannel('storage', storageChannel); sharedProcessClient.then(client => client.registerChannel('storage', storageChannel)); // Profile Storage Changes Listener (shared process) - const profileStorageListener = this._register(new ProfileStorageChangesListenerChannel(accessor.get(IStorageMainService), accessor.get(IUserDataProfilesMainService), this.logService)); + const profileStorageListener = disposables.add((new ProfileStorageChangesListenerChannel(accessor.get(IStorageMainService), accessor.get(IUserDataProfilesMainService), this.logService))); sharedProcessClient.then(client => client.registerChannel('profileStorageListener', profileStorageListener)); // Terminal @@ -1356,7 +1334,7 @@ export class CodeApplication extends Disposable { return windowsMainService.open({ context, cli: args, - forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), + forceNewWindow: args['new-window'], diffMode: args.diff, mergeMode: args.merge, noRecentEntry, @@ -1391,10 +1369,10 @@ export class CodeApplication extends Disposable { // Crash reporter this.updateCrashReporterEnablement(); + // macOS: rosetta translation warning if (isMacintosh && app.runningUnderARM64Translation) { this.windowsMainService?.sendToFocused('vscode:showTranslatedBuildWarning'); } - } private async installMutex(): Promise { @@ -1434,7 +1412,7 @@ export class CodeApplication extends Disposable { try { const argvContent = await this.fileService.readFile(this.environmentMainService.argvResource); const argvString = argvContent.value.toString(); - const argvJSON = JSON.parse(stripComments(argvString)); + const argvJSON = parse(argvString); const telemetryLevel = getTelemetryLevel(this.configurationService); const enableCrashReporter = telemetryLevel >= TelemetryLevel.CRASH; @@ -1465,6 +1443,9 @@ export class CodeApplication extends Disposable { } } catch (error) { this.logService.error(error); + + // Inform the user via notification + this.windowsMainService?.sendToFocused('vscode:showArgvParseWarning'); } } } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 8548fa7ce4d..deb0d1eab56 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -6,7 +6,7 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import { unlinkSync } from 'fs'; +import { unlinkSync, promises } from 'fs'; import { URI } from 'vs/base/common/uri'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { Promises } from 'vs/base/common/async'; @@ -139,7 +139,7 @@ class CodeMain { Event.once(lifecycleMainService.onWillShutdown)(evt => { fileService.dispose(); configurationService.dispose(); - evt.join('instanceLockfile', FSPromises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ })); + evt.join('instanceLockfile', promises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ })); }); return instantiationService.createInstance(CodeApplication, mainProcessNodeIpcServer, instanceEnvironment).startup(); @@ -257,7 +257,7 @@ class CodeMain { environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.localHistoryHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.backupHome - ].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)), + ].map(path => path ? promises.mkdir(path, { recursive: true }) : undefined)), // State service stateService.init(), diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html index 55f2c0fe81c..dd0548c22c1 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html @@ -35,8 +35,7 @@ - - + diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 8234b734d06..98e67e7c25c 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -4,8 +4,13 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check +'use strict'; + (function () { - 'use strict'; + + /** + * @import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes' + */ const bootstrapWindow = bootstrapWindowLib(); @@ -21,12 +26,10 @@ }); /** - * @typedef {import('../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, + * resultCallback: (result: any, configuration: ISandboxConfiguration) => unknown, * options?: { * configureDeveloperSettings?: (config: ISandboxConfiguration) => { * forceEnableDeveloperKeybindings?: boolean, diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.html index a92bea242a4..b1be2b7527c 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench-dev.html +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.html @@ -68,8 +68,7 @@ - - + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 35e8368d3c9..5ad97839d23 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -6,21 +6,27 @@ /// //@ts-check +'use strict'; + (function () { - 'use strict'; + + /** + * @import {INativeWindowConfiguration} from '../../../platform/window/common/window' + * @import {NativeParsedArgs} from '../../../platform/environment/common/argv' + * @import {ISandboxConfiguration} from '../../../base/parts/sandbox/common/sandboxTypes' + */ const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top performance.mark('code/didStartRenderer'); - // Load workbench main JS, CSS and NLS all in parallel. This is an + // Load workbench main JS and CSS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because // we know for a fact that workbench.desktop.main will depend on - // the related CSS and NLS counterparts. + // the related CSS counterpart. bootstrapWindow.load([ 'vs/workbench/workbench.desktop.main', - 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], function (desktopMain, configuration) { @@ -45,6 +51,7 @@ showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { + // @ts-ignore loaderConfig.recordStats = true; }, beforeRequire: function (windowConfig) { @@ -74,14 +81,10 @@ //#region Helpers /** - * @typedef {import('../../../platform/window/common/window').INativeWindowConfiguration} INativeWindowConfiguration - * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs - * @typedef {import('../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, + * resultCallback: (result: any, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, * options?: { * configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => { * forceDisableShowDevtoolsOnError?: boolean, @@ -129,7 +132,9 @@ } // minimal color configuration (works with or without persisted data) - let baseTheme, shellBackground, shellForeground; + let baseTheme; + let shellBackground; + let shellForeground; if (data) { baseTheme = data.baseTheme; shellBackground = data.colorInfo.editorBackground; @@ -162,7 +167,9 @@ style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; // set zoom level as soon as possible + // @ts-ignore if (typeof data?.zoomLevel === 'number' && typeof globalThis.vscode?.webFrame?.setZoomLevel === 'function') { + // @ts-ignore globalThis.vscode.webFrame.setZoomLevel(data.zoomLevel); } @@ -172,9 +179,9 @@ const splash = document.createElement('div'); splash.id = 'monaco-parts-splash'; - splash.className = baseTheme; + splash.className = baseTheme ?? 'vs-dark'; - if (layoutInfo.windowBorder) { + if (layoutInfo.windowBorder && colorInfo.windowBorder) { splash.style.position = 'relative'; splash.style.height = 'calc(100vh - 2px)'; splash.style.width = 'calc(100vw - 2px)'; diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 578bf1a282c..9acf37f930f 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -39,10 +39,6 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { || !!argv['telemetry']; } -interface IMainCli { - main: (argv: NativeParsedArgs) => Promise; -} - export async function main(argv: string[]): Promise { let args: NativeParsedArgs; @@ -59,19 +55,28 @@ export async function main(argv: string[]): Promise { console.error(`'${subcommand}' command not supported in ${product.applicationName}`); return; } + const env: IProcessEnvironment = { + ...process.env + }; + // bootstrap-amd.js determines the electron environment based + // on the following variable. For the server we need to unset + // it to prevent importing any electron specific modules. + // Refs https://github.com/microsoft/vscode/issues/221883 + delete env['ELECTRON_RUN_AS_NODE']; + const tunnelArgs = argv.slice(argv.indexOf(subcommand) + 1); // all arguments behind `tunnel` return new Promise((resolve, reject) => { let tunnelProcess: ChildProcess; const stdio: StdioOptions = ['ignore', 'pipe', 'pipe']; if (process.env['VSCODE_DEV']) { - tunnelProcess = spawn('cargo', ['run', '--', subcommand, ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio }); + tunnelProcess = spawn('cargo', ['run', '--', subcommand, ...tunnelArgs], { cwd: join(getAppRoot(), 'cli'), stdio, env }); } else { const appPath = process.platform === 'darwin' // ./Contents/MacOS/Electron => ./Contents/Resources/app/bin/code-tunnel-insiders ? join(dirname(dirname(process.execPath)), 'Resources', 'app') : dirname(process.execPath); const tunnelCommand = join(appPath, 'bin', `${product.tunnelApplicationName}${isWindows ? '.exe' : ''}`); - tunnelProcess = spawn(tunnelCommand, [subcommand, ...tunnelArgs], { cwd: cwd(), stdio }); + tunnelProcess = spawn(tunnelCommand, [subcommand, ...tunnelArgs], { cwd: cwd(), stdio, env }); } tunnelProcess.stdout!.pipe(process.stdout); @@ -112,7 +117,8 @@ export async function main(argv: string[]): Promise { // Extensions Management else if (shouldSpawnCliProcess(args)) { - const cli = await new Promise((resolve, reject) => require(['vs/code/node/cliProcessMain'], resolve, reject)); + + const cli = await import('vs/code/node/cliProcessMain'); await cli.main(args); return; @@ -324,6 +330,7 @@ export async function main(argv: string[]): Promise { // to get better profile traces. Last, we listen on stdout for a signal that tells us to // stop profiling. if (args['prof-startup']) { + const profileHost = '127.0.0.1'; const portMain = await findFreePort(randomPort(), 10, 3000); const portRenderer = await findFreePort(portMain + 1, 10, 3000); const portExthost = await findFreePort(portRenderer + 1, 10, 3000); @@ -335,9 +342,9 @@ export async function main(argv: string[]): Promise { const filenamePrefix = randomPath(homedir(), 'prof'); - addArg(argv, `--inspect-brk=${portMain}`); - addArg(argv, `--remote-debugging-port=${portRenderer}`); - addArg(argv, `--inspect-brk-extensions=${portExthost}`); + addArg(argv, `--inspect-brk=${profileHost}:${portMain}`); + addArg(argv, `--remote-debugging-port=${profileHost}:${portRenderer}`); + addArg(argv, `--inspect-brk-extensions=${profileHost}:${portExthost}`); addArg(argv, `--prof-startup-prefix`, filenamePrefix); addArg(argv, `--no-cached-data`); @@ -351,7 +358,7 @@ export async function main(argv: string[]): Promise { let session: ProfilingSession; try { - session = await profiler.startProfiling(opts); + session = await profiler.startProfiling({ ...opts, host: profileHost }); } catch (err) { console.error(`FAILED to start profiling for '${name}' on port '${opts.port}'`); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index aea83578ee0..f940e372ab3 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { hostname, release } from 'os'; import { raceTimeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -13,7 +14,6 @@ import { isAbsolute, join } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; import { URI } from 'vs/base/common/uri'; -import { Promises } from 'vs/base/node/pfs'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { IDownloadService } from 'vs/platform/download/common/download'; @@ -57,7 +57,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesReadonlyService } from 'vs/platform/userDataProfile/node/userDataProfile'; -import { resolveMachineId, resolveSqmId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolvedevDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; @@ -124,7 +124,7 @@ class CliMain extends Disposable { await Promise.all([ this.allowWindowsUNCPath(environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath), this.allowWindowsUNCPath(environmentService.extensionsPath) - ].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); + ].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); // Logger const loggerService = new LoggerService(getLogLevel(environmentService), environmentService.logsHome); @@ -186,6 +186,7 @@ class CliMain extends Disposable { } } const sqmId = await resolveSqmId(stateService, logService); + const devDeviceId = await resolvedevDeviceId(stateService, logService); // Initialize user data profiles after initializing the state userDataProfilesService.init(); @@ -221,7 +222,7 @@ class CliMain extends Disposable { const config: ITelemetryServiceConfig = { appenders, sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, isInternal), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, devDeviceId, isInternal), piiPaths: getPiiPathsFromEnvironment(environmentService) }; diff --git a/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts b/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts index 77ae5d9786a..7ca147e384d 100644 --- a/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts +++ b/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { RunOnceScheduler } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -54,7 +55,7 @@ export class CodeCacheCleaner extends Disposable { // Delete cache folder if old enough const codeCacheEntryPath = join(codeCacheRootPath, codeCache); - const codeCacheEntryStat = await Promises.stat(codeCacheEntryPath); + const codeCacheEntryStat = await fs.promises.stat(codeCacheEntryPath); if (codeCacheEntryStat.isDirectory() && (now - codeCacheEntryStat.mtime.getTime()) > this._DataMaxAge) { this.logService.trace(`[code cache cleanup]: Removing code cache folder ${codeCache}.`); diff --git a/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 68d2cc16661..0c7e517d4bc 100644 --- a/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IStringDictionary } from 'vs/base/common/collections'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -58,7 +59,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { try { const installed: IStringDictionary = Object.create(null); - const metaData: ILanguagePackFile = JSON.parse(await Promises.readFile(join(this.environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + const metaData: ILanguagePackFile = JSON.parse(await fs.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; @@ -93,7 +94,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { } const candidate = join(folder, entry); - const stat = await Promises.stat(candidate); + const stat = await fs.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)}`); diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 9a59ccce5c3..69c81ac2498 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -71,7 +71,7 @@ import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; -import { NativeUserDataProfileStorageService } from 'vs/platform/userDataProfile/node/userDataProfileStorageService'; +import { SharedProcessUserDataProfileStorageService } from 'vs/platform/userDataProfile/node/userDataProfileStorageService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; @@ -307,7 +307,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { telemetryService = new TelemetryService({ appenders, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, internalTelemetry), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, this.configuration.devDeviceId, internalTelemetry), sendErrorTelemetry: true, piiPaths: getPiiPathsFromEnvironment(environmentService), }, configurationService, productService); @@ -355,7 +355,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(IUserDataSyncLocalStoreService, new SyncDescriptor(UserDataSyncLocalStoreService, undefined, false /* Eagerly cleans up old backups */)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService, undefined, true)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService, undefined, false /* Initializes the Sync State */)); - services.set(IUserDataProfileStorageService, new SyncDescriptor(NativeUserDataProfileStorageService, undefined, true)); + services.set(IUserDataProfileStorageService, new SyncDescriptor(SharedProcessUserDataProfileStorageService, undefined, true)); services.set(IUserDataSyncResourceProviderService, new SyncDescriptor(UserDataSyncResourceProviderService, undefined, true)); // Signing diff --git a/src/vs/editor/browser/config/charWidthReader.ts b/src/vs/editor/browser/config/charWidthReader.ts index 90bafb66284..e1d36f142bf 100644 --- a/src/vs/editor/browser/config/charWidthReader.ts +++ b/src/vs/editor/browser/config/charWidthReader.ts @@ -56,7 +56,7 @@ class DomCharWidthReader { this._readFromDomElements(); // Remove the container from the DOM - targetWindow.document.body.removeChild(this._container!); + this._container?.remove(); this._container = null; this._testElements = null; diff --git a/src/vs/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts index 06e8339cbc7..4de800bd521 100644 --- a/src/vs/editor/browser/config/editorConfiguration.ts +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -21,6 +21,7 @@ import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguratio import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { getWindow, getWindowById } from 'vs/base/browser/dom'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; +import { MenuId } from 'vs/platform/actions/common/actions'; export interface IEditorConstructionOptions extends IEditorOptions { /** @@ -43,6 +44,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat public readonly onDidChangeFast: Event = this._onDidChangeFast.event; public readonly isSimpleWidget: boolean; + public readonly contextMenuId: MenuId; private readonly _containerObserver: ElementSizeObserver; private _isDominatedByLongLines: boolean = false; @@ -68,12 +70,14 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat constructor( isSimpleWidget: boolean, + contextMenuId: MenuId, options: Readonly, container: HTMLElement | null, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { super(); this.isSimpleWidget = isSimpleWidget; + this.contextMenuId = contextMenuId; this._containerObserver = this._register(new ElementSizeObserver(container, options.dimension)); this._targetWindowId = getWindow(container).vscodeWindowId; diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index 1ae9ecab361..2019016ac4c 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -33,7 +33,7 @@ export class PointerEventHandler extends MouseHandler { this._lastPointerType = 'mouse'; this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, 'pointerdown', (e: any) => { - const pointerType = e.pointerType; + const pointerType = e.pointerType; if (pointerType === 'mouse') { this._lastPointerType = 'mouse'; return; diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index c8e5b7e50a4..a5f824ca450 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -950,7 +950,7 @@ function measureText(targetDocument: Document, text: string, fontInfo: FontInfo, const res = regularDomNode.offsetWidth; - targetDocument.body.removeChild(container); + container.remove(); return res; } diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 88b4eb324e6..88e783a1f7d 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -196,7 +196,7 @@ export class TextAreaInput extends Disposable { private readonly _asyncTriggerCut: RunOnceScheduler; - private _asyncFocusGainWriteScreenReaderContent: MutableDisposable = this._register(new MutableDisposable()); + private readonly _asyncFocusGainWriteScreenReaderContent: MutableDisposable = this._register(new MutableDisposable()); private _textAreaState: TextAreaState; diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index ca0a4bb8a1f..e7f0743af83 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -30,6 +30,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IViewModel } from 'vs/editor/common/viewModel'; import { ISelection } from 'vs/editor/common/core/selection'; import { getActiveElement } from 'vs/base/browser/dom'; +import { EnterOperation } from 'vs/editor/common/cursor/cursorTypeEditOperations'; const CORE_WEIGHT = KeybindingWeight.EditorCore; @@ -74,7 +75,7 @@ export namespace EditorScroll_ { return true; }; - export const metadata = { + export const metadata: ICommandMetadata = { description: 'Scroll editor in the given direction', args: [ { @@ -252,7 +253,7 @@ export namespace RevealLine_ { return true; }; - export const metadata = { + export const metadata: ICommandMetadata = { description: 'Reveal the given line at the given logical position', args: [ { @@ -1988,7 +1989,7 @@ export namespace CoreEditingCommands { public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); + editor.executeCommands(this.id, EnterOperation.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); } }); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 7678c6e3b88..2985f959335 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -22,6 +22,7 @@ import { InjectedText } from 'vs/editor/common/modelLineProjectionData'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; import { IEditorWhitespace, IViewModel } from 'vs/editor/common/viewModel'; import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -249,11 +250,21 @@ export interface IOverlayWidgetPosition { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: Event; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ @@ -571,6 +582,11 @@ export interface ICodeEditor extends editorCommon.IEditor { * @internal */ readonly isSimpleWidget: boolean; + /** + * The context menu ID that should be used to lookup context menu actions. + * @internal + */ + readonly contextMenuId: MenuId; /** * The editor's scoped context key service. * @internal @@ -768,6 +784,20 @@ export interface ICodeEditor extends editorCommon.IEditor { */ readonly onDidChangeHiddenAreas: Event; + /** + * Some editor operations fire multiple events at once. + * To allow users to react to multiple events fired by a single operation, + * the editor fires a begin update before the operation and an end update after the operation. + * Whenever the editor fires `onBeginUpdate`, it will also fire `onEndUpdate` once the operation finishes. + * Note that not all operations are bracketed by `onBeginUpdate` and `onEndUpdate`. + */ + readonly onBeginUpdate: Event; + + /** + * Fires after the editor completes the operation it fired `onBeginUpdate` for. + */ + readonly onEndUpdate: Event; + /** * Saves current view state of the editor in a serializable object. */ diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index f295dd8b43d..a894d0034ec 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -359,7 +359,7 @@ export interface CssProperties { class RefCountedCssRule { private _referenceCount: number = 0; private _styleElement: HTMLStyleElement | undefined; - private _styleElementDisposables: DisposableStore; + private readonly _styleElementDisposables: DisposableStore; constructor( public readonly key: string, diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts new file mode 100644 index 00000000000..4bca7d678e9 --- /dev/null +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equalsIfDefined, itemsEquals } from 'vs/base/common/equals'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ITransaction, autorun, autorunOpts, autorunWithStoreHandleChanges, derived, derivedOpts, observableFromEvent, observableSignal, observableValue, observableValueOpts } from 'vs/base/common/observable'; +import { TransactionImpl } from 'vs/base/common/observableInternal/base'; +import { derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { EditorOption, FindComputedEditorOptionValueById } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; + +/** + * Returns a facade for the code editor that provides observables for various states/events. +*/ +export function observableCodeEditor(editor: ICodeEditor): ObservableCodeEditor { + return ObservableCodeEditor.get(editor); +} + +export class ObservableCodeEditor extends Disposable { + private static readonly _map = new Map(); + + /** + * Make sure that editor is not disposed yet! + */ + public static get(editor: ICodeEditor): ObservableCodeEditor { + let result = ObservableCodeEditor._map.get(editor); + if (!result) { + result = new ObservableCodeEditor(editor); + ObservableCodeEditor._map.set(editor, result); + const d = editor.onDidDispose(() => { + const item = ObservableCodeEditor._map.get(editor); + if (item) { + ObservableCodeEditor._map.delete(editor); + item.dispose(); + d.dispose(); + } + }); + } + return result; + } + + private _updateCounter = 0; + private _currentTransaction: TransactionImpl | undefined = undefined; + + private _beginUpdate(): void { + this._updateCounter++; + if (this._updateCounter === 1) { + this._currentTransaction = new TransactionImpl(() => { + /** @description Update editor state */ + }); + } + } + + private _endUpdate(): void { + this._updateCounter--; + if (this._updateCounter === 0) { + const t = this._currentTransaction!; + this._currentTransaction = undefined; + t.finish(); + } + } + + private constructor(public readonly editor: ICodeEditor) { + super(); + + this._register(this.editor.onBeginUpdate(() => this._beginUpdate())); + this._register(this.editor.onEndUpdate(() => this._endUpdate())); + + this._register(this.editor.onDidChangeModel(() => { + this._beginUpdate(); + try { + this._model.set(this.editor.getModel(), this._currentTransaction); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidType((e) => { + this._beginUpdate(); + try { + this._forceUpdate(); + this.onDidType.trigger(this._currentTransaction, e); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidChangeModelContent(e => { + this._beginUpdate(); + try { + this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidChangeCursorSelection(e => { + this._beginUpdate(); + try { + this._selections.set(this.editor.getSelections(), this._currentTransaction, e); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + } + + public forceUpdate(): void; + public forceUpdate(cb: (tx: ITransaction) => T): T; + public forceUpdate(cb?: (tx: ITransaction) => T): T { + this._beginUpdate(); + try { + this._forceUpdate(); + if (!cb) { return undefined as T; } + return cb(this._currentTransaction!); + } finally { + this._endUpdate(); + } + } + + private _forceUpdate(): void { + this._beginUpdate(); + try { + this._model.set(this.editor.getModel(), this._currentTransaction); + this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined); + this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined); + } finally { + this._endUpdate(); + } + } + + private readonly _model = observableValue(this, this.editor.getModel()); + public readonly model: IObservable = this._model; + + public readonly isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); + + private readonly _versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null); + public readonly versionId: IObservable = this._versionId; + + private readonly _selections = observableValueOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, + this.editor.getSelections() ?? null + ); + public readonly selections: IObservable = this._selections; + + + public readonly positions = derivedOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) }, + reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null + ); + + public readonly isFocused = observableFromEvent(this, e => { + const d1 = this.editor.onDidFocusEditorWidget(e); + const d2 = this.editor.onDidBlurEditorWidget(e); + return { + dispose() { + d1.dispose(); + d2.dispose(); + } + }; + }, () => this.editor.hasWidgetFocus()); + + public readonly value = derivedWithSetter(this, + reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; }, + (value, tx) => { + const model = this.model.get(); + if (model !== null) { + if (value !== model.getValue()) { + model.setValue(value); + } + } + } + ); + public readonly valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; }); + public readonly cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); + public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); + + public readonly onDidType = observableSignal(this); + + public readonly scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop()); + public readonly scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); + + public readonly layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); + public readonly layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft); + public readonly layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); + + public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); + + public getOption(id: T): IObservable> { + return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => { + if (e.hasChanged(id)) { cb(undefined); } + }), () => this.editor.getOption(id)); + } + + public setDecorations(decorations: IObservable): IDisposable { + const d = new DisposableStore(); + const decorationsCollection = this.editor.createDecorationsCollection(); + d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => { + const d = decorations.read(reader); + decorationsCollection.set(d); + })); + d.add({ + dispose: () => { + decorationsCollection.clear(); + } + }); + return d; + } + + private _overlayWidgetCounter = 0; + + public createOverlayWidget(widget: IObservableOverlayWidget): IDisposable { + const overlayWidgetId = 'observableOverlayWidget' + (this._overlayWidgetCounter++); + const w: IOverlayWidget = { + getDomNode: () => widget.domNode, + getPosition: () => widget.position.get(), + getId: () => overlayWidgetId, + allowEditorOverflow: widget.allowEditorOverflow, + getMinContentWidthInPx: () => widget.minContentWidthInPx.get(), + }; + this.editor.addOverlayWidget(w); + const d = autorun(reader => { + widget.position.read(reader); + widget.minContentWidthInPx.read(reader); + this.editor.layoutOverlayWidget(w); + }); + return toDisposable(() => { + d.dispose(); + this.editor.removeOverlayWidget(w); + }); + } +} + +interface IObservableOverlayWidget { + get domNode(): HTMLElement; + readonly position: IObservable; + readonly minContentWidthInPx: IObservable; + get allowEditorOverflow(): boolean; +} + +type RemoveUndefined = T extends undefined ? never : T; +export function reactToChange(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[]) => void): IDisposable { + return autorunWithStoreHandleChanges({ + createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), + handleChange: (context, changeSummary) => { + if (context.didChange(observable)) { + const e = context.change; + if (e !== undefined) { + changeSummary.deltas.push(e as RemoveUndefined); + } + changeSummary.didChange = true; + } + return true; + }, + }, (reader, changeSummary) => { + const value = observable.read(reader); + if (changeSummary.didChange) { + cb(value, changeSummary.deltas); + } + }); +} + +export function reactToChangeWithStore(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { + const store = new DisposableStore(); + const disposable = reactToChange(observable, (value, deltas) => { + store.clear(); + cb(value, deltas, store); + }); + return { + dispose() { + disposable.dispose(); + store.dispose(); + } + }; +} diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index b960f48191e..1fedb4d17c4 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -341,7 +341,7 @@ class RefCountedStyleSheet { public unref(): void { this._refCount--; if (this._refCount === 0) { - this._styleSheet.parentNode?.removeChild(this._styleSheet); + this._styleSheet.remove(); this._parent._removeEditorStyleSheets(this._editorId); } } diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index 38357608b86..20faf6cfa8c 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -6,21 +6,24 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IHoverService, IHoverOptions } from 'vs/platform/hover/browser/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { HoverWidget } from 'vs/editor/browser/services/hoverService/hoverWidget'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow, isHTMLElement } from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { mainWindow } from 'vs/base/browser/window'; -import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ContextViewHandler } from 'vs/platform/contextview/browser/contextViewService'; +import type { IHoverOptions, IHoverWidget, IManagedHover, IManagedHoverContentOrFactory, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverDelegate, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ManagedHoverWidget } from 'vs/editor/browser/services/hoverService/updatableHoverWidget'; +import { TimeoutTimer } from 'vs/base/common/async'; export class HoverService extends Disposable implements IHoverService { declare readonly _serviceBrand: undefined; @@ -59,7 +62,9 @@ export class HoverService extends Disposable implements IHoverService { // HACK, remove this check when #189076 is fixed if (!skipLastFocusedUpdate) { if (trapFocus && activeElement) { - this._lastFocusedElementBeforeOpen = activeElement as HTMLElement; + if (!activeElement.classList.contains('monaco-hover')) { + this._lastFocusedElementBeforeOpen = activeElement as HTMLElement; + } } else { this._lastFocusedElementBeforeOpen = undefined; } @@ -82,10 +87,10 @@ export class HoverService extends Disposable implements IHoverService { this._currentHoverOptions = undefined; } hoverDisposables.dispose(); - }); + }, undefined, hoverDisposables); // Set the container explicitly to enable aux window support if (!options.container) { - const targetElement = options.target instanceof HTMLElement ? options.target : options.target.targetElements[0]; + const targetElement = isHTMLElement(options.target) ? options.target : options.target.targetElements[0]; options.container = this._layoutService.getContainer(getWindow(targetElement)); } @@ -93,7 +98,7 @@ export class HoverService extends Disposable implements IHoverService { new HoverContextViewDelegate(hover, focus), options.container ); - hover.onRequestLayout(() => this._contextViewHandler.layout()); + hover.onRequestLayout(() => this._contextViewHandler.layout(), undefined, hoverDisposables); if (options.persistence?.sticky) { hoverDisposables.add(addDisposableListener(getWindow(options.container).document, EventType.MOUSE_DOWN, e => { if (!isAncestor(e.target as HTMLElement, hover.domNode)) { @@ -183,6 +188,153 @@ export class HoverService extends Disposable implements IHoverService { } } } + + private readonly _managedHovers = new Map(); + + // TODO: Investigate performance of this function. There seems to be a lot of content created + // and thrown away on start up + setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions | undefined): IManagedHover { + + targetElement.setAttribute('custom-hover', 'true'); + + if (targetElement.title !== '') { + console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.'); + console.trace('Stack trace:', targetElement.title); + targetElement.title = ''; + } + + let hoverPreparation: IDisposable | undefined; + let hoverWidget: ManagedHoverWidget | undefined; + + const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => { + const hadHover = hoverWidget !== undefined; + if (disposeWidget) { + hoverWidget?.dispose(); + hoverWidget = undefined; + } + if (disposePreparation) { + hoverPreparation?.dispose(); + hoverPreparation = undefined; + } + if (hadHover) { + hoverDelegate.onDidHideHover?.(); + hoverWidget = undefined; + } + }; + + const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget, trapFocus?: boolean) => { + return new TimeoutTimer(async () => { + if (!hoverWidget || hoverWidget.isDisposed) { + hoverWidget = new ManagedHoverWidget(hoverDelegate, target || targetElement, delay > 0); + await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus }); + } + }, delay); + }; + + let isMouseDown = false; + const mouseDownEmitter = addDisposableListener(targetElement, EventType.MOUSE_DOWN, () => { + isMouseDown = true; + hideHover(true, true); + }, true); + const mouseUpEmitter = addDisposableListener(targetElement, EventType.MOUSE_UP, () => { + isMouseDown = false; + }, true); + const mouseLeaveEmitter = addDisposableListener(targetElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => { + isMouseDown = false; + hideHover(false, (e).fromElement === targetElement); + }, true); + + const onMouseOver = (e: MouseEvent) => { + if (hoverPreparation) { + return; + } + + const toDispose: DisposableStore = new DisposableStore(); + + const target: IHoverDelegateTarget = { + targetElements: [targetElement], + dispose: () => { } + }; + if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') { + // track the mouse position + const onMouseMove = (e: MouseEvent) => { + target.x = e.x + 10; + if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target, targetElement) !== targetElement) { + hideHover(true, true); + } + }; + toDispose.add(addDisposableListener(targetElement, EventType.MOUSE_MOVE, onMouseMove, true)); + } + + hoverPreparation = toDispose; + + if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target as HTMLElement, targetElement) !== targetElement) { + return; // Do not show hover when the mouse is over another hover target + } + + toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); + }; + const mouseOverDomEmitter = addDisposableListener(targetElement, EventType.MOUSE_OVER, onMouseOver, true); + + const onFocus = () => { + if (isMouseDown || hoverPreparation) { + return; + } + const target: IHoverDelegateTarget = { + targetElements: [targetElement], + dispose: () => { } + }; + const toDispose: DisposableStore = new DisposableStore(); + const onBlur = () => hideHover(true, true); + toDispose.add(addDisposableListener(targetElement, EventType.BLUR, onBlur, true)); + toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); + hoverPreparation = toDispose; + }; + + // Do not show hover when focusing an input or textarea + let focusDomEmitter: undefined | IDisposable; + const tagName = targetElement.tagName.toLowerCase(); + if (tagName !== 'input' && tagName !== 'textarea') { + focusDomEmitter = addDisposableListener(targetElement, EventType.FOCUS, onFocus, true); + } + + const hover: IManagedHover = { + show: focus => { + hideHover(false, true); // terminate a ongoing mouse over preparation + triggerShowHover(0, focus, undefined, focus); // show hover immediately + }, + hide: () => { + hideHover(true, true); + }, + update: async (newContent, hoverOptions) => { + content = newContent; + await hoverWidget?.update(content, undefined, hoverOptions); + }, + dispose: () => { + this._managedHovers.delete(targetElement); + mouseOverDomEmitter.dispose(); + mouseLeaveEmitter.dispose(); + mouseDownEmitter.dispose(); + mouseUpEmitter.dispose(); + focusDomEmitter?.dispose(); + hideHover(true, true); + } + }; + this._managedHovers.set(targetElement, hover); + return hover; + } + + showManagedHover(target: HTMLElement): void { + const hover = this._managedHovers.get(target); + if (hover) { + hover.show(true); + } + } + + public override dispose(): void { + this._managedHovers.forEach(hover => hover.dispose()); + super.dispose(); + } } function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined { @@ -227,6 +379,14 @@ class HoverContextViewDelegate implements IDelegate { } } +function getHoverTargetElement(element: HTMLElement, stopElement?: HTMLElement): HTMLElement { + stopElement = stopElement ?? getWindow(element).document.body; + while (!element.hasAttribute('custom-hover') && element !== stopElement) { + element = element.parentElement!; + } + return element; +} + registerSingleton(IHoverService, HoverService, InstantiationType.Delayed); registerThemingParticipant((theme, collector) => { diff --git a/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts index 24ae9cf483e..ed929bc965c 100644 --- a/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -8,7 +8,6 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as dom from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IHoverTarget, IHoverOptions } from 'vs/platform/hover/browser/hover'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -23,7 +22,7 @@ import { localize } from 'vs/nls'; import { isMacintosh } from 'vs/base/common/platform'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { status } from 'vs/base/browser/ui/aria/aria'; -import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import type { IHoverOptions, IHoverTarget, IHoverWidget } from 'vs/base/browser/ui/hover/hover'; const $ = dom.$; type TargetRect = { @@ -151,7 +150,7 @@ export class HoverWidget extends Widget implements IHoverWidget { contentsElement.textContent = options.content; contentsElement.style.whiteSpace = 'pre-wrap'; - } else if (options.content instanceof HTMLElement) { + } else if (dom.isHTMLElement(options.content)) { contentsElement.appendChild(options.content); contentsElement.classList.add('html-hover-contents'); diff --git a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts new file mode 100644 index 00000000000..46f7423aa3a --- /dev/null +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isHTMLElement } from 'vs/base/browser/dom'; +import type { IHoverWidget, IManagedHoverContent, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { isMarkdownString, type IMarkdownString } from 'vs/base/common/htmlContent'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { isFunction, isString } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; + +type IManagedHoverResolvedContent = IMarkdownString | string | HTMLElement | undefined; + +export class ManagedHoverWidget implements IDisposable { + + private _hoverWidget: IHoverWidget | undefined; + private _cancellationTokenSource: CancellationTokenSource | undefined; + + constructor(private hoverDelegate: IHoverDelegate, private target: IHoverDelegateTarget | HTMLElement, private fadeInAnimation: boolean) { + } + + async update(content: IManagedHoverContent, focus?: boolean, options?: IManagedHoverOptions): Promise { + if (this._cancellationTokenSource) { + // there's an computation ongoing, cancel it + this._cancellationTokenSource.dispose(true); + this._cancellationTokenSource = undefined; + } + if (this.isDisposed) { + return; + } + + let resolvedContent; + if (content === undefined || isString(content) || isHTMLElement(content)) { + 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); + } + + // compute the content + this._cancellationTokenSource = new CancellationTokenSource(); + const token = this._cancellationTokenSource.token; + resolvedContent = await content.markdown(token); + if (resolvedContent === undefined) { + resolvedContent = content.markdownNotSupportedFallback; + } + + if (this.isDisposed || token.isCancellationRequested) { + // either the widget has been closed in the meantime + // or there has been a new call to `update` + return; + } + } + + this.show(resolvedContent, focus, options); + } + + private show(content: IManagedHoverResolvedContent, focus?: boolean, options?: IManagedHoverOptions): void { + const oldHoverWidget = this._hoverWidget; + + if (this.hasContent(content)) { + const hoverOptions: IHoverDelegateOptions = { + content, + target: this.target, + actions: options?.actions, + linkHandler: options?.linkHandler, + trapFocus: options?.trapFocus, + appearance: { + showPointer: this.hoverDelegate.placement === 'element', + skipFadeInAnimation: !this.fadeInAnimation || !!oldHoverWidget, // do not fade in if the hover is already showing + showHoverHint: options?.appearance?.showHoverHint, + }, + position: { + hoverPosition: HoverPosition.BELOW, + }, + }; + + this._hoverWidget = this.hoverDelegate.showHover(hoverOptions, focus); + } + oldHoverWidget?.dispose(); + } + + private hasContent(content: IManagedHoverResolvedContent): content is NonNullable { + if (!content) { + return false; + } + + if (isMarkdownString(content)) { + return !!content.value; + } + + return true; + } + + get isDisposed() { + return this._hoverWidget?.isDisposed; + } + + dispose(): void { + this._hoverWidget?.dispose(); + this._cancellationTokenSource?.dispose(true); + this._cancellationTokenSource = undefined; + } +} diff --git a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts new file mode 100644 index 00000000000..f60a51bfb42 --- /dev/null +++ b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts @@ -0,0 +1,364 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TreeSitterTokenizationRegistry } from 'vs/editor/common/languages'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { AppResourcePath, FileAccess, nodeModulesPath } from 'vs/base/common/network'; +import { ITreeSitterParserService } from 'vs/editor/common/services/treeSitterParserService'; +import { IModelService } from 'vs/editor/common/services/model'; +import { Disposable, DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ITextModel } from 'vs/editor/common/model'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { setTimeout0 } from 'vs/base/common/platform'; +import { importAMDNodeModule } from 'vs/amdX'; +import { Event } from 'vs/base/common/event'; +import { cancelOnDispose } from 'vs/base/common/cancellation'; + +const EDITOR_EXPERIMENTAL_PREFER_TREESITTER = 'editor.experimental.preferTreeSitter'; +const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; +const moduleLocationTreeSitter: AppResourcePath = `${nodeModulesPath}/@vscode/tree-sitter-wasm/wasm`; +const moduleLocationTreeSitterWasm: AppResourcePath = `${moduleLocationTreeSitter}/tree-sitter.wasm`; + +export class TextModelTreeSitter extends Disposable { + private _treeSitterTree: TreeSitterTree | undefined; + + // Not currently used since we just get telemetry, but later this will be needed. + get tree() { return this._treeSitterTree; } + + constructor(readonly model: ITextModel, + private readonly _treeSitterParser: TreeSitterLanguages, + private readonly _treeSitterImporter: TreeSitterImporter, + private readonly _logService: ILogService, + private readonly _telemetryService: ITelemetryService + ) { + super(); + this._register(Event.runAndSubscribe(this.model.onDidChangeLanguage, (e => this._onDidChangeLanguage(e ? e.newLanguage : this.model.getLanguageId())))); + } + + private readonly _languageSessionDisposables = this._register(new DisposableStore()); + /** + * Be very careful when making changes to this method as it is easy to introduce race conditions. + */ + private async _onDidChangeLanguage(languageId: string) { + this._languageSessionDisposables.clear(); + this._treeSitterTree = undefined; + + const token = cancelOnDispose(this._languageSessionDisposables); + const language = await this._treeSitterParser.getLanguage(languageId); + if (!language || token.isCancellationRequested) { + return; + } + + const Parser = await this._treeSitterImporter.getParserClass(); + if (token.isCancellationRequested) { + return; + } + + const treeSitterTree = this._languageSessionDisposables.add(new TreeSitterTree(new Parser(), language, this._logService, this._telemetryService)); + this._languageSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e))); + await this._onDidChangeContent(treeSitterTree); + if (token.isCancellationRequested) { + return; + } + + this._treeSitterTree = treeSitterTree; + } + + private async _onDidChangeContent(treeSitterTree: TreeSitterTree, e?: IModelContentChangedEvent) { + return treeSitterTree.onDidChangeContent(this.model, e); + } +} + +const enum TelemetryParseType { + Full = 'fullParse', + Incremental = 'incrementalParse' +} + +export class TreeSitterTree implements IDisposable { + private _tree: Parser.Tree | undefined; + private _isDisposed: boolean = false; + constructor(public readonly parser: Parser, + public /** exposed for tests **/ readonly language: Parser.Language, + private readonly _logService: ILogService, + private readonly _telemetryService: ITelemetryService) { + this.parser.setTimeoutMicros(50 * 1000); // 50 ms + this.parser.setLanguage(language); + } + dispose(): void { + this._isDisposed = true; + this._tree?.delete(); + this.parser?.delete(); + } + get tree() { return this._tree; } + set tree(newTree: Parser.Tree | undefined) { + this._tree?.delete(); + this._tree = newTree; + } + get isDisposed() { return this._isDisposed; } + + private _onDidChangeContentQueue: Promise = Promise.resolve(); + public async onDidChangeContent(model: ITextModel, e?: IModelContentChangedEvent) { + this._onDidChangeContentQueue = this._onDidChangeContentQueue.then(() => { + if (this.isDisposed) { + // No need to continue the queue if we are disposed + return; + } + return this._onDidChangeContent(model, e); + }).catch((e) => { + this._logService.error('Error parsing tree-sitter tree', e); + }); + return this._onDidChangeContentQueue; + } + + private async _onDidChangeContent(model: ITextModel, e?: IModelContentChangedEvent) { + if (e) { + for (const change of e.changes) { + const newEndOffset = change.rangeOffset + change.text.length; + const newEndPosition = model.getPositionAt(newEndOffset); + + this.tree?.edit({ + startIndex: change.rangeOffset, + oldEndIndex: change.rangeOffset + change.rangeLength, + newEndIndex: change.rangeOffset + change.text.length, + startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, + oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, + newEndPosition: { row: newEndPosition.lineNumber - 1, column: newEndPosition.column - 1 } + }); + } + } + + this.tree = await this.parse(model); + } + + private parse(model: ITextModel): Promise { + let parseType: TelemetryParseType = TelemetryParseType.Full; + if (this.tree) { + parseType = TelemetryParseType.Incremental; + } + return this._parseAndYield(model, parseType); + } + + private async _parseAndYield(model: ITextModel, parseType: TelemetryParseType): Promise { + const language = model.getLanguageId(); + let tree: Parser.Tree | undefined; + let time: number = 0; + let passes: number = 0; + do { + const timer = performance.now(); + try { + tree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this.tree); + } catch (e) { + // parsing can fail when the timeout is reached, will resume upon next loop + } finally { + time += performance.now() - timer; + passes++; + } + + // Even if the model changes and edits are applied, the tree parsing will continue correctly after the await. + await new Promise(resolve => setTimeout0(resolve)); + + if (model.isDisposed() || this.isDisposed) { + return; + } + } while (!tree); + this.sendParseTimeTelemetry(parseType, language, time, passes); + return tree; + } + + private _parseCallback(textModel: ITextModel, index: number): string | null { + return textModel.getTextBuffer().getNearestChunk(index); + } + + private sendParseTimeTelemetry(parseType: TelemetryParseType, languageId: string, time: number, passes: number): void { + this._logService.debug(`Tree parsing (${parseType}) took ${time} ms and ${passes} passes.`); + type ParseTimeClassification = { + owner: 'alros'; + comment: 'Used to understand how long it takes to parse a tree-sitter tree'; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The programming language ID.' }; + time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ms it took to parse' }; + passes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of passes it took to parse' }; + }; + if (parseType === TelemetryParseType.Full) { + this._telemetryService.publicLog2<{ languageId: string; time: number; passes: number }, ParseTimeClassification>(`treeSitter.fullParse`, { languageId, time, passes }); + } else { + this._telemetryService.publicLog2<{ languageId: string; time: number; passes: number }, ParseTimeClassification>(`treeSitter.incrementalParse`, { languageId, time, passes }); + } + } +} + +export class TreeSitterLanguages extends Disposable { + private _languages: Map = new Map(); + + constructor(private readonly _treeSitterImporter: TreeSitterImporter, + private readonly _fileService: IFileService + ) { + super(); + } + + public async getLanguage(languageId: string): Promise { + let language = this._languages.get(languageId); + if (!language) { + language = await this._fetchLanguage(languageId); + if (!language) { + return undefined; + } + this._languages.set(languageId, language); + } + return language; + } + + private async _fetchLanguage(languageId: string): Promise { + const grammarName = TreeSitterTokenizationRegistry.get(languageId); + const languageLocation = this._getLanguageLocation(languageId); + if (!grammarName || !languageLocation) { + return undefined; + } + const wasmPath: AppResourcePath = `${languageLocation}/${grammarName.name}.wasm`; + const languageFile = await (this._fileService.readFile(FileAccess.asFileUri(wasmPath))); + const Parser = await this._treeSitterImporter.getParserClass(); + return Parser.Language.load(languageFile.value.buffer); + } + + private _getLanguageLocation(languageId: string): AppResourcePath | undefined { + const grammarName = TreeSitterTokenizationRegistry.get(languageId); + if (!grammarName) { + return undefined; + } + return moduleLocationTreeSitter; + } +} + +export class TreeSitterImporter { + private _treeSitterImport: typeof import('@vscode/tree-sitter-wasm') | undefined; + private async _getTreeSitterImport() { + if (!this._treeSitterImport) { + this._treeSitterImport = await importAMDNodeModule('@vscode/tree-sitter-wasm', 'wasm/tree-sitter.js'); + } + return this._treeSitterImport; + } + + private _parserClass: typeof Parser | undefined; + public async getParserClass() { + if (!this._parserClass) { + this._parserClass = (await this._getTreeSitterImport()).Parser; + } + return this._parserClass; + } +} + +export class TreeSitterTextModelService extends Disposable implements ITreeSitterParserService { + readonly _serviceBrand: undefined; + private _init!: Promise; + private _textModelTreeSitters: DisposableMap = this._register(new DisposableMap()); + private _registeredLanguages: DisposableMap = this._register(new DisposableMap()); + private readonly _treeSitterImporter: TreeSitterImporter = new TreeSitterImporter(); + private readonly _treeSitterParser: TreeSitterLanguages; + + constructor(@IModelService private readonly _modelService: IModelService, + @IFileService fileService: IFileService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + this._treeSitterParser = this._register(new TreeSitterLanguages(this._treeSitterImporter, fileService)); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(EDITOR_EXPERIMENTAL_PREFER_TREESITTER)) { + this._supportedLanguagesChanged(); + } + })); + this._supportedLanguagesChanged(); + } + + private async _doInitParser() { + const Parser = await this._treeSitterImporter.getParserClass(); + await Parser.init({ + locateFile(_file: string, _folder: string) { + return FileAccess.asBrowserUri(moduleLocationTreeSitterWasm).toString(true); + } + }); + return true; + } + + private _hasInit: boolean = false; + private async _initParser(hasLanguages: boolean): Promise { + if (this._hasInit) { + return this._init; + } + + if (hasLanguages) { + this._hasInit = true; + this._init = this._doInitParser(); + + // New init, we need to deal with all the existing text models and set up listeners + this._init.then(() => this._registerModelServiceListeners()); + } else { + this._init = Promise.resolve(false); + } + return this._init; + } + + private async _supportedLanguagesChanged() { + const setting = this._getSetting(); + + let hasLanguages = true; + if (setting.length === 0) { + hasLanguages = false; + } + + if (await this._initParser(hasLanguages)) { + // Eventually, this should actually use an extension point to add tree sitter grammars, but for now they are hard coded in core + if (setting.includes('typescript')) { + this._addGrammar('typescript', 'tree-sitter-typescript'); + } else { + this._removeGrammar('typescript'); + } + } + } + + private _getSetting(): string[] { + const setting = this._configurationService.getValue(EDITOR_EXPERIMENTAL_PREFER_TREESITTER); + if (setting && setting.length > 0) { + return setting; + } else { + const expSetting = this._configurationService.getValue(EDITOR_TREESITTER_TELEMETRY); + if (expSetting) { + return ['typescript']; + } + } + return []; + } + + private async _registerModelServiceListeners() { + this._register(this._modelService.onModelAdded(model => { + this._createTextModelTreeSitter(model); + })); + this._register(this._modelService.onModelRemoved(model => { + this._textModelTreeSitters.deleteAndDispose(model); + })); + this._modelService.getModels().forEach(model => this._createTextModelTreeSitter(model)); + } + + private _createTextModelTreeSitter(model: ITextModel) { + const textModelTreeSitter = new TextModelTreeSitter(model, this._treeSitterParser, this._treeSitterImporter, this._logService, this._telemetryService); + this._textModelTreeSitters.set(model, textModelTreeSitter); + } + + private _addGrammar(languageId: string, grammarName: string) { + if (!TreeSitterTokenizationRegistry.get(languageId)) { + this._registeredLanguages.set(languageId, TreeSitterTokenizationRegistry.register(languageId, { name: grammarName })); + } + } + + private _removeGrammar(languageId: string) { + if (this._registeredLanguages.has(languageId)) { + this._registeredLanguages.deleteAndDispose('typescript'); + } + } +} diff --git a/src/vs/editor/browser/stableEditorScroll.ts b/src/vs/editor/browser/stableEditorScroll.ts index 9f145b5ea64..986e18ac6c0 100644 --- a/src/vs/editor/browser/stableEditorScroll.ts +++ b/src/vs/editor/browser/stableEditorScroll.ts @@ -5,6 +5,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; +import { ScrollType } from 'vs/editor/common/editorCommon'; export class StableEditorScrollState { @@ -59,6 +60,47 @@ export class StableEditorScrollState { } const offset = editor.getTopForLineNumber(currentCursorPosition.lineNumber) - editor.getTopForLineNumber(this._cursorPosition.lineNumber); - editor.setScrollTop(editor.getScrollTop() + offset); + editor.setScrollTop(editor.getScrollTop() + offset, ScrollType.Immediate); + } +} + + +export class StableEditorBottomScrollState { + + public static capture(editor: ICodeEditor): StableEditorBottomScrollState { + if (editor.hasPendingScrollAnimation()) { + // Never mess with the scroll if there is a pending scroll animation + return new StableEditorBottomScrollState(editor.getScrollTop(), editor.getContentHeight(), null, 0); + } + + let visiblePosition: Position | null = null; + let visiblePositionScrollDelta = 0; + const visibleRanges = editor.getVisibleRanges(); + if (visibleRanges.length > 0) { + visiblePosition = visibleRanges.at(-1)!.getEndPosition(); + const visiblePositionScrollBottom = editor.getBottomForLineNumber(visiblePosition.lineNumber); + visiblePositionScrollDelta = visiblePositionScrollBottom - editor.getScrollTop(); + } + return new StableEditorBottomScrollState(editor.getScrollTop(), editor.getContentHeight(), visiblePosition, visiblePositionScrollDelta); + } + + constructor( + private readonly _initialScrollTop: number, + private readonly _initialContentHeight: number, + private readonly _visiblePosition: Position | null, + private readonly _visiblePositionScrollDelta: number, + ) { + } + + public restore(editor: ICodeEditor): void { + if (this._initialContentHeight === editor.getContentHeight() && this._initialScrollTop === editor.getScrollTop()) { + // The editor's content height and scroll top haven't changed, so we don't need to do anything + return; + } + + if (this._visiblePosition) { + const visiblePositionScrollBottom = editor.getBottomForLineNumber(this._visiblePosition.lineNumber); + editor.setScrollTop(visiblePositionScrollBottom - this._visiblePositionScrollDelta, ScrollType.Immediate); + } } } diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index a803234c4b4..5558cf28cd4 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -634,8 +634,7 @@ export class View extends ViewEventHandler { } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { - const newPreference = widgetData.position ? widgetData.position.preference : null; - const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, widgetData.position); if (shouldRender) { this._scheduleRender(); } diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 64fb2185eed..2861fd8f82a 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -184,7 +184,7 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength); } - targetWindow.document.body.removeChild(containerDomNode); + containerDomNode.remove(); return result; } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index bbbb0dd9d73..971d8ae4011 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -295,9 +295,7 @@ export class VisibleLinesCollection { // Remove from DOM for (let i = 0, len = deleted.length; i < len; i++) { const lineDomNode = deleted[i].getDomNode(); - if (lineDomNode) { - this.domNode.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } } @@ -310,9 +308,7 @@ export class VisibleLinesCollection { // Remove from DOM for (let i = 0, len = deleted.length; i < len; i++) { const lineDomNode = deleted[i].getDomNode(); - if (lineDomNode) { - this.domNode.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } } @@ -481,9 +477,7 @@ class ViewLayerRenderer { private _removeLinesBefore(ctx: IRendererContext, removeCount: number): void { for (let i = 0; i < removeCount; i++) { const lineDomNode = ctx.lines[i].getDomNode(); - if (lineDomNode) { - this.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } ctx.lines.splice(0, removeCount); } @@ -502,9 +496,7 @@ class ViewLayerRenderer { for (let i = 0; i < removeCount; i++) { const lineDomNode = ctx.lines[removeIndex + i].getDomNode(); - if (lineDomNode) { - this.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } ctx.lines.splice(removeIndex, removeCount); } diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index 3112c19c324..bdf8eb77d21 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -121,7 +121,7 @@ export class ViewContentWidgets extends ViewPart { delete this._widgets[widgetId]; const domNode = myWidget.domNode.domNode; - domNode.parentNode!.removeChild(domNode); + domNode.remove(); domNode.removeAttribute('monaco-visible-content-widget'); this.setShouldRender(); @@ -167,11 +167,19 @@ interface IBoxLayoutResult { left: number; } -interface IRenderData { +interface IOffViewportRenderData { + kind: 'offViewport'; + preserveFocus: boolean; +} + +interface IInViewportRenderData { + kind: 'inViewport'; coordinate: Coordinate; position: ContentWidgetPositionPreference; } +type IRenderData = IInViewportRenderData | IOffViewportRenderData; + class Widget { private readonly _context: ViewContext; private readonly _viewDomNode: FastDomNode; @@ -435,7 +443,11 @@ class Widget { const { primary, secondary } = this._getAnchorsCoordinates(ctx); if (!primary) { - return null; + return { + kind: 'offViewport', + preserveFocus: this.domNode.domNode.contains(this.domNode.domNode.ownerDocument.activeElement) + }; + // return null; } if (this._cachedDomNodeOffsetWidth === -1 || this._cachedDomNodeOffsetHeight === -1) { @@ -474,7 +486,11 @@ class Widget { return null; } if (pass === 2 || placement.fitsAbove) { - return { coordinate: new Coordinate(placement.aboveTop, placement.left), position: ContentWidgetPositionPreference.ABOVE }; + return { + kind: 'inViewport', + coordinate: new Coordinate(placement.aboveTop, placement.left), + position: ContentWidgetPositionPreference.ABOVE + }; } } else if (pref === ContentWidgetPositionPreference.BELOW) { if (!placement) { @@ -482,13 +498,25 @@ class Widget { return null; } if (pass === 2 || placement.fitsBelow) { - return { coordinate: new Coordinate(placement.belowTop, placement.left), position: ContentWidgetPositionPreference.BELOW }; + return { + kind: 'inViewport', + coordinate: new Coordinate(placement.belowTop, placement.left), + position: ContentWidgetPositionPreference.BELOW + }; } } else { if (this.allowEditorOverflow) { - return { coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)), position: ContentWidgetPositionPreference.EXACT }; + return { + kind: 'inViewport', + coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)), + position: ContentWidgetPositionPreference.EXACT + }; } else { - return { coordinate: new Coordinate(anchor.top, anchor.left), position: ContentWidgetPositionPreference.EXACT }; + return { + kind: 'inViewport', + coordinate: new Coordinate(anchor.top, anchor.left), + position: ContentWidgetPositionPreference.EXACT + }; } } } @@ -518,12 +546,19 @@ class Widget { } public render(ctx: RestrictedRenderingContext): void { - if (!this._renderData) { + if (!this._renderData || this._renderData.kind === 'offViewport') { // This widget should be invisible if (this._isVisible) { this.domNode.removeAttribute('monaco-visible-content-widget'); this._isVisible = false; - this.domNode.setVisibility('hidden'); + + if (this._renderData?.kind === 'offViewport' && this._renderData.preserveFocus) { + // widget wants to be shown, but it is outside of the viewport and it + // has focus which we need to preserve + this.domNode.setTop(-1000); + } else { + this.domNode.setVisibility('hidden'); + } } if (typeof this._actual.afterRender === 'function') { diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 48c79a783ea..164b2299b75 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -243,7 +243,7 @@ export class GlyphMarginWidgets extends ViewPart { const domNode = widgetData.domNode.domNode; delete this._widgets[widgetId]; - domNode.parentNode?.removeChild(domNode); + domNode.remove(); this.setShouldRender(); } } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 427df7cd3d8..7b49f5b0dea 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -94,6 +94,10 @@ class MinimapOptions { public readonly minimapCharWidth: number; public readonly sectionHeaderFontFamily: string; public readonly sectionHeaderFontSize: number; + /** + * Space in between the characters of the section header (in CSS px) + */ + public readonly sectionHeaderLetterSpacing: number; public readonly sectionHeaderFontColor: RGBA8; public readonly charRenderer: () => MinimapCharRenderer; @@ -139,6 +143,7 @@ class MinimapOptions { this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; this.sectionHeaderFontFamily = DEFAULT_FONT_FAMILY; this.sectionHeaderFontSize = minimapOpts.sectionHeaderFontSize * pixelRatio; + this.sectionHeaderLetterSpacing = minimapOpts.sectionHeaderLetterSpacing; // intentionally not multiplying by pixelRatio this.sectionHeaderFontColor = MinimapOptions._getSectionHeaderColor(theme, tokensColorTracker.getColor(ColorId.DefaultForeground)); this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); @@ -196,6 +201,7 @@ class MinimapOptions { && this.minimapLineHeight === other.minimapLineHeight && this.minimapCharWidth === other.minimapCharWidth && this.sectionHeaderFontSize === other.sectionHeaderFontSize + && this.sectionHeaderLetterSpacing === other.sectionHeaderLetterSpacing && this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor) && this.backgroundColor && this.backgroundColor.equals(other.backgroundColor) && this.foregroundAlpha === other.foregroundAlpha @@ -1788,6 +1794,7 @@ class InnerMinimap extends Disposable { private _renderSectionHeaders(layout: MinimapLayout) { const minimapLineHeight = this._model.options.minimapLineHeight; const sectionHeaderFontSize = this._model.options.sectionHeaderFontSize; + const sectionHeaderLetterSpacing = this._model.options.sectionHeaderLetterSpacing; const backgroundFillHeight = sectionHeaderFontSize * 1.5; const { canvasInnerWidth } = this._model.options; @@ -1798,7 +1805,8 @@ class InnerMinimap extends Disposable { const separatorStroke = foregroundFill; const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!; - canvasContext.font = sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily; + canvasContext.letterSpacing = sectionHeaderLetterSpacing + 'px'; + canvasContext.font = '500 ' + sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily; canvasContext.strokeStyle = separatorStroke; canvasContext.lineWidth = 0.2; diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 0953248e2ab..5b3e86a042d 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -5,7 +5,7 @@ import 'vs/css!./overlayWidgets'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { IOverlayWidget, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; @@ -17,6 +17,7 @@ import * as dom from 'vs/base/browser/dom'; interface IWidgetData { widget: IOverlayWidget; preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + stack?: number; domNode: FastDomNode; } @@ -109,14 +110,17 @@ export class ViewOverlayWidgets extends ViewPart { this._updateMaxMinWidth(); } - public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null): boolean { + public setWidgetPosition(widget: IOverlayWidget, position: IOverlayWidgetPosition | null): boolean { const widgetData = this._widgets[widget.getId()]; - if (widgetData.preference === preference) { + const preference = position ? position.preference : null; + const stack = position?.stackOridinal; + if (widgetData.preference === preference && widgetData.stack === stack) { this._updateMaxMinWidth(); return false; } widgetData.preference = preference; + widgetData.stack = stack; this.setShouldRender(); this._updateMaxMinWidth(); @@ -150,7 +154,7 @@ export class ViewOverlayWidgets extends ViewPart { this._context.viewLayout.setOverlayWidgetsMinWidth(maxMinWidth); } - private _renderWidget(widgetData: IWidgetData): void { + private _renderWidget(widgetData: IWidgetData, stackCoordinates: number[]): void { const domNode = widgetData.domNode; if (widgetData.preference === null) { @@ -158,16 +162,29 @@ export class ViewOverlayWidgets extends ViewPart { return; } - if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) { - domNode.setTop(0); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); - } else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - const widgetHeight = domNode.domNode.clientHeight; - domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); + const maxRight = (2 * this._verticalScrollbarWidth) + this._minimapWidth; + if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER || widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + const widgetHeight = domNode.domNode.clientHeight; + domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); + } else { + domNode.setTop(0); + } + + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[widgetData.preference]); + stackCoordinates[widgetData.preference] += domNode.domNode.clientWidth; + } else { + domNode.setRight(maxRight); + } } else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) { - domNode.setTop(0); domNode.domNode.style.right = '50%'; + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER]); + stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER] += domNode.domNode.clientHeight; + } else { + domNode.setTop(0); + } } else { const { top, left } = widgetData.preference; const fixedOverflowWidgets = this._context.configuration.options.get(EditorOption.fixedOverflowWidgets); @@ -194,9 +211,12 @@ export class ViewOverlayWidgets extends ViewPart { this._domNode.setWidth(this._editorWidth); const keys = Object.keys(this._widgets); + const stackCoordinates = Array.from({ length: OverlayWidgetPositionPreference.TOP_CENTER + 1 }, () => 0); + keys.sort((a, b) => (this._widgets[a].stack || 0) - (this._widgets[b].stack || 0)); + for (let i = 0, len = keys.length; i < len; i++) { const widgetId = keys[i]; - this._renderWidget(this._widgets[widgetId]); + this._renderWidget(this._widgets[widgetId], stackCoordinates); } } } diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 0ca31065a45..8e3eb5666fa 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -512,9 +512,7 @@ export class DecorationsOverviewRuler extends ViewPart { canvasCtx.strokeStyle = this._settings.borderColor; canvasCtx.moveTo(0, 0); canvasCtx.lineTo(0, canvasHeight); - canvasCtx.stroke(); - - canvasCtx.moveTo(0, 0); + canvasCtx.moveTo(1, 0); canvasCtx.lineTo(canvasWidth, 0); canvasCtx.stroke(); } diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 37914a70335..a99dac77cde 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -271,12 +271,12 @@ export class ViewZones extends ViewPart { zone.domNode.removeAttribute('monaco-visible-view-zone'); zone.domNode.removeAttribute('monaco-view-zone'); - zone.domNode.domNode.parentNode!.removeChild(zone.domNode.domNode); + zone.domNode.domNode.remove(); if (zone.marginDomNode) { zone.marginDomNode.removeAttribute('monaco-visible-view-zone'); zone.marginDomNode.removeAttribute('monaco-view-zone'); - zone.marginDomNode.domNode.parentNode!.removeChild(zone.marginDomNode.domNode); + zone.marginDomNode.domNode.remove(); } this.setShouldRender(); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 108aa52fa8a..f8ed64fba6c 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -59,6 +59,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { editorErrorForeground, editorHintForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { MenuId } from 'vs/platform/actions/common/actions'; export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeEditor { @@ -184,12 +185,25 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidChangeHiddenAreas: Emitter = this._register(new Emitter({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeHiddenAreas: Event = this._onDidChangeHiddenAreas.event; + + private _updateCounter = 0; + + private readonly _onBeginUpdate: Emitter = this._register(new Emitter()); + public readonly onBeginUpdate: Event = this._onBeginUpdate.event; + + private readonly _onEndUpdate: Emitter = this._register(new Emitter()); + public readonly onEndUpdate: Event = this._onEndUpdate.event; + //#endregion public get isSimpleWidget(): boolean { return this._configuration.isSimpleWidget; } + public get contextMenuId(): MenuId { + return this._configuration.contextMenuId; + } + private readonly _telemetryData?: object; private readonly _domElement: HTMLElement; @@ -254,7 +268,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._decorationTypeSubtypes = {}; this._telemetryData = codeEditorWidgetOptions.telemetryData; - this._configuration = this._register(this._createConfiguration(codeEditorWidgetOptions.isSimpleWidget || false, options, accessibilityService)); + this._configuration = this._register(this._createConfiguration(codeEditorWidgetOptions.isSimpleWidget || false, + codeEditorWidgetOptions.contextMenuId ?? (codeEditorWidgetOptions.isSimpleWidget ? MenuId.SimpleEditorContext : MenuId.EditorContext), + options, accessibilityService)); this._register(this._configuration.onDidChange((e) => { this._onDidChangeConfiguration.fire(e); @@ -273,7 +289,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._register(new EditorContextKeysManager(this, this._contextKeyService)); this._register(new EditorModeContext(this, this._contextKeyService, languageFeaturesService)); - this._instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService])); + this._instantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService]))); this._modelData = null; @@ -362,8 +378,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._modelData?.view.writeScreenReaderContent(reason); } - protected _createConfiguration(isSimpleWidget: boolean, options: Readonly, accessibilityService: IAccessibilityService): EditorConfiguration { - return new EditorConfiguration(isSimpleWidget, options, this._domElement, accessibilityService); + protected _createConfiguration(isSimpleWidget: boolean, contextMenuId: MenuId, options: Readonly, accessibilityService: IAccessibilityService): EditorConfiguration { + return new EditorConfiguration(isSimpleWidget, contextMenuId, options, this._domElement, accessibilityService); } public getId(): string { @@ -437,10 +453,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } public setValue(newValue: string): void { - if (!this._modelData) { - return; + try { + this._beginUpdate(); + if (!this._modelData) { + return; + } + this._modelData.model.setValue(newValue); + } finally { + this._endUpdate(); } - this._modelData.model.setValue(newValue); } public getModel(): ITextModel | null { @@ -451,34 +472,39 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } public setModel(_model: ITextModel | editorCommon.IDiffEditorModel | editorCommon.IDiffEditorViewModel | null = null): void { - const model = _model; - if (this._modelData === null && model === null) { - // Current model is the new model - return; + try { + this._beginUpdate(); + const model = _model; + if (this._modelData === null && model === null) { + // Current model is the new model + return; + } + if (this._modelData && this._modelData.model === model) { + // Current model is the new model + return; + } + + const e: editorCommon.IModelChangedEvent = { + oldModelUrl: this._modelData?.model.uri || null, + newModelUrl: model?.uri || null + }; + this._onWillChangeModel.fire(e); + + const hasTextFocus = this.hasTextFocus(); + const detachedModel = this._detachModel(); + this._attachModel(model); + if (hasTextFocus && this.hasModel()) { + this.focus(); + } + + this._removeDecorationTypes(); + this._onDidChangeModel.fire(e); + this._postDetachModelCleanup(detachedModel); + + this._contributionsDisposable = this._contributions.onAfterModelAttached(); + } finally { + this._endUpdate(); } - if (this._modelData && this._modelData.model === model) { - // Current model is the new model - return; - } - - const e: editorCommon.IModelChangedEvent = { - oldModelUrl: this._modelData?.model.uri || null, - newModelUrl: model?.uri || null - }; - this._onWillChangeModel.fire(e); - - const hasTextFocus = this.hasTextFocus(); - const detachedModel = this._detachModel(); - this._attachModel(model); - if (hasTextFocus && this.hasModel()) { - this.focus(); - } - - this._removeDecorationTypes(); - this._onDidChangeModel.fire(e); - this._postDetachModelCleanup(detachedModel); - - this._contributionsDisposable = this._contributions.onAfterModelAttached(); } private _removeDecorationTypes(): void { @@ -1019,53 +1045,59 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public trigger(source: string | null | undefined, handlerId: string, payload: any): void { payload = payload || {}; - switch (handlerId) { - case editorCommon.Handler.CompositionStart: - this._startComposition(); - return; - case editorCommon.Handler.CompositionEnd: - this._endComposition(source); - return; - case editorCommon.Handler.Type: { - const args = >payload; - this._type(source, args.text || ''); + try { + this._beginUpdate(); + + switch (handlerId) { + case editorCommon.Handler.CompositionStart: + this._startComposition(); + return; + case editorCommon.Handler.CompositionEnd: + this._endComposition(source); + return; + case editorCommon.Handler.Type: { + const args = >payload; + this._type(source, args.text || ''); + return; + } + case editorCommon.Handler.ReplacePreviousChar: { + const args = >payload; + this._compositionType(source, args.text || '', args.replaceCharCnt || 0, 0, 0); + return; + } + case editorCommon.Handler.CompositionType: { + const args = >payload; + this._compositionType(source, args.text || '', args.replacePrevCharCnt || 0, args.replaceNextCharCnt || 0, args.positionDelta || 0); + return; + } + case editorCommon.Handler.Paste: { + const args = >payload; + this._paste(source, args.text || '', args.pasteOnNewLine || false, args.multicursorText || null, args.mode || null, args.clipboardEvent); + return; + } + case editorCommon.Handler.Cut: + this._cut(source); + return; + } + + const action = this.getAction(handlerId); + if (action) { + Promise.resolve(action.run(payload)).then(undefined, onUnexpectedError); return; } - case editorCommon.Handler.ReplacePreviousChar: { - const args = >payload; - this._compositionType(source, args.text || '', args.replaceCharCnt || 0, 0, 0); + + if (!this._modelData) { return; } - case editorCommon.Handler.CompositionType: { - const args = >payload; - this._compositionType(source, args.text || '', args.replacePrevCharCnt || 0, args.replaceNextCharCnt || 0, args.positionDelta || 0); + + if (this._triggerEditorCommand(source, handlerId, payload)) { return; } - case editorCommon.Handler.Paste: { - const args = >payload; - this._paste(source, args.text || '', args.pasteOnNewLine || false, args.multicursorText || null, args.mode || null, args.clipboardEvent); - return; - } - case editorCommon.Handler.Cut: - this._cut(source); - return; - } - const action = this.getAction(handlerId); - if (action) { - Promise.resolve(action.run(payload)).then(undefined, onUnexpectedError); - return; + this._triggerCommand(handlerId, payload); + } finally { + this._endUpdate(); } - - if (!this._modelData) { - return; - } - - if (this._triggerEditorCommand(source, handlerId, payload)) { - return; - } - - this._triggerCommand(handlerId, payload); } protected _triggerCommand(handlerId: string, payload: any): void { @@ -1563,7 +1595,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData || !this._modelData.hasRealView) { return; } - this._modelData.view.render(true, forceRedraw); + this._modelData.viewModel.batchEvents(() => { + this._modelData!.view.render(true, forceRedraw); + }); } public setAriaOptions(options: editorBrowser.IEditorAriaOptions): void { @@ -1579,7 +1613,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public setBanner(domNode: HTMLElement | null, domNodeHeight: number): void { if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { - this._domElement.removeChild(this._bannerDomNode); + this._bannerDomNode.remove(); } this._bannerDomNode = domNode; @@ -1614,6 +1648,16 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this.languageConfigurationService, this._themeService, attachedView, + { + batchChanges: (cb) => { + try { + this._beginUpdate(); + return cb(); + } finally { + this._endUpdate(); + } + }, + } ); // Someone might destroy the model from under the editor, so prevent any exceptions by setting a null model @@ -1840,10 +1884,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._domElement.removeAttribute('data-mode-id'); if (removeDomNode && this._domElement.contains(removeDomNode)) { - this._domElement.removeChild(removeDomNode); + removeDomNode.remove(); } if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { - this._domElement.removeChild(this._bannerDomNode); + this._bannerDomNode.remove(); } return model; } @@ -1885,6 +1929,20 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public setContextValue(key: string, value: ContextKeyValue): void { this._contextKeyService.createKey(key, value); } + + private _beginUpdate(): void { + this._updateCounter++; + if (this._updateCounter === 1) { + this._onBeginUpdate.fire(); + } + } + + private _endUpdate(): void { + this._updateCounter--; + if (this._updateCounter === 0) { + this._onEndUpdate.fire(); + } + } } let EDITOR_ID = 0; @@ -1909,6 +1967,12 @@ export interface ICodeEditorWidgetOptions { * Defaults to null. */ telemetryData?: object; + + /** + * The ID of the context menu. + * Defaults to MenuId.SimpleEditorContext or MenuId.EditorContext depending on whether the widget is simple. + */ + contextMenuId?: MenuId; } class ModelData { diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index 09c4a32f141..a6d82d5845f 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -23,6 +23,7 @@ -webkit-text-size-adjust: 100%; color: var(--vscode-editor-foreground); background-color: var(--vscode-editor-background); + overflow-wrap: initial; } .monaco-editor-background { background-color: var(--vscode-editor-background); @@ -61,10 +62,6 @@ width: 100%; } -.monaco-editor .view-overlays > div > div, .monaco-editor .margin-view-overlays > div > div { - bottom: 0; -} - /* .monaco-editor .auto-closed-character { opacity: 0.3; diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts index a5992771a9a..efbbe00f6c1 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorDecorations.ts @@ -6,6 +6,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, derived } from 'vs/base/common/observable'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; +import { allowsTrueInlineDiffRendering } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; @@ -28,7 +29,8 @@ export class DiffEditorDecorations extends Disposable { } private readonly _decorations = derived(this, (reader) => { - const diff = this._diffModel.read(reader)?.diff.read(reader); + const diffModel = this._diffModel.read(reader); + const diff = diffModel?.diff.read(reader); if (!diff) { return null; } @@ -56,13 +58,29 @@ export class DiffEditorDecorations extends Disposable { modifiedDecorations.push({ range: m.lineRangeMapping.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); } } else { + const useInlineDiff = this._options.useTrueInlineDiffRendering.read(reader) && allowsTrueInlineDiffRendering(m.lineRangeMapping); for (const i of m.lineRangeMapping.innerChanges || []) { // Don't show empty markers outside the line range if (m.lineRangeMapping.original.contains(i.originalRange.startLineNumber)) { originalDecorations.push({ range: i.originalRange, options: (i.originalRange.isEmpty() && showEmptyDecorations) ? diffDeleteDecorationEmpty : diffDeleteDecoration }); } if (m.lineRangeMapping.modified.contains(i.modifiedRange.startLineNumber)) { - modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations) ? diffAddDecorationEmpty : diffAddDecoration }); + modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations && !useInlineDiff) ? diffAddDecorationEmpty : diffAddDecoration }); + } + if (useInlineDiff) { + const deletedText = diffModel!.model.original.getValueInRange(i.originalRange); + modifiedDecorations.push({ + range: i.modifiedRange, + options: { + description: 'deleted-text', + before: { + content: deletedText, + inlineClassName: 'inline-deleted-text', + }, + zIndex: 100000, + showIfCollapsed: true, + } + }); } } } diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index 2abc8e74bad..ee7935fd520 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IReader, autorunHandleChanges, derived, derivedOpts, observableFromEvent } from 'vs/base/common/observable'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; @@ -26,18 +27,21 @@ export class DiffEditorEditors extends Disposable { private readonly _onDidContentSizeChange = this._register(new Emitter()); public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; } - public readonly modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); - public readonly modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); + public readonly modifiedScrollTop = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); + public readonly modifiedScrollHeight = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); - public readonly modifiedModel = observableFromEvent(this.modified.onDidChangeModel, () => /** @description modified.model */ this.modified.getModel()); + public readonly modifiedObs = observableCodeEditor(this.modified); + public readonly originalObs = observableCodeEditor(this.original); - public readonly modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); - public readonly modifiedCursor = derivedOpts({ owner: this, equalityComparer: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); + public readonly modifiedModel = this.modifiedObs.model; - public readonly originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); + public readonly modifiedSelections = observableFromEvent(this, this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); + public readonly modifiedCursor = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); - public readonly isOriginalFocused = observableFromEvent(Event.any(this.original.onDidFocusEditorWidget, this.original.onDidBlurEditorWidget), () => this.original.hasWidgetFocus()); - public readonly isModifiedFocused = observableFromEvent(Event.any(this.modified.onDidFocusEditorWidget, this.modified.onDidBlurEditorWidget), () => this.modified.hasWidgetFocus()); + public readonly originalCursor = observableFromEvent(this, this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); + + public readonly isOriginalFocused = observableCodeEditor(this.original).isFocused; + public readonly isModifiedFocused = observableCodeEditor(this.modified).isFocused; public readonly isFocused = derived(this, reader => this.isOriginalFocused.read(reader) || this.isModifiedFocused.read(reader)); @@ -55,7 +59,7 @@ export class DiffEditorEditors extends Disposable { this._argCodeEditorWidgetOptions = null as any; this._register(autorunHandleChanges({ - createEmptyChangeSummary: () => ({} as IDiffEditorConstructionOptions), + createEmptyChangeSummary: (): IDiffEditorConstructionOptions => ({}), handleChange: (ctx, changeSummary) => { if (ctx.didChange(_options.editorOptions)) { Object.assign(changeSummary, ctx.change.changedOptions); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts index cb4e31b2fe3..3361129a083 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts @@ -5,64 +5,34 @@ import { IBoundarySashes, ISashEvent, Orientation, Sash, SashState } from 'vs/base/browser/ui/sash/sash'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, autorun, derived, observableValue } from 'vs/base/common/observable'; +import { IObservable, IReader, ISettableObservable, autorun, observableValue } from 'vs/base/common/observable'; import { DiffEditorOptions } from '../diffEditorOptions'; +import { derivedWithSetter } from 'vs/base/common/observableInternal/derived'; -export class DiffEditorSash extends Disposable { - private readonly _sashRatio = observableValue(this, undefined); - - public readonly sashLeft = derived(this, reader => { +export class SashLayout { + public readonly sashLeft = derivedWithSetter(this, reader => { const ratio = this._sashRatio.read(reader) ?? this._options.splitViewDefaultRatio.read(reader); return this._computeSashLeft(ratio, reader); + }, (value, tx) => { + const contentWidth = this.dimensions.width.get(); + this._sashRatio.set(value / contentWidth, tx); }); - private readonly _sash = this._register(new Sash(this._domNode, { - getVerticalSashTop: (_sash: Sash): number => 0, - getVerticalSashLeft: (_sash: Sash): number => this.sashLeft.get(), - getVerticalSashHeight: (_sash: Sash): number => this._dimensions.height.get(), - }, { orientation: Orientation.VERTICAL })); + private readonly _sashRatio = observableValue(this, undefined); - private _startSashPosition: number | undefined = undefined; + public resetSash(): void { + this._sashRatio.set(undefined, undefined); + } constructor( private readonly _options: DiffEditorOptions, - private readonly _domNode: HTMLElement, - private readonly _dimensions: { height: IObservable; width: IObservable }, - private readonly _sashes: IObservable, + public readonly dimensions: { height: IObservable; width: IObservable }, ) { - super(); - - this._register(this._sash.onDidStart(() => { - this._startSashPosition = this.sashLeft.get(); - })); - this._register(this._sash.onDidChange((e: ISashEvent) => { - const contentWidth = this._dimensions.width.get(); - const sashPosition = this._computeSashLeft((this._startSashPosition! + (e.currentX - e.startX)) / contentWidth, undefined); - this._sashRatio.set(sashPosition / contentWidth, undefined); - })); - this._register(this._sash.onDidEnd(() => this._sash.layout())); - this._register(this._sash.onDidReset(() => this._sashRatio.set(undefined, undefined))); - - this._register(autorun(reader => { - const sashes = this._sashes.read(reader); - if (sashes) { - this._sash.orthogonalEndSash = sashes.bottom; - } - })); - - this._register(autorun(reader => { - /** @description DiffEditorSash.layoutSash */ - const enabled = this._options.enableSplitViewResizing.read(reader); - this._sash.state = enabled ? SashState.Enabled : SashState.Disabled; - this.sashLeft.read(reader); - this._dimensions.height.read(reader); - this._sash.layout(); - })); } /** @pure */ private _computeSashLeft(desiredRatio: number, reader: IReader | undefined): number { - const contentWidth = this._dimensions.width.read(reader); + const contentWidth = this.dimensions.width.read(reader); const midPoint = Math.floor(this._options.splitViewDefaultRatio.read(reader) * contentWidth); const sashLeft = this._options.enableSplitViewResizing.read(reader) ? Math.floor(desiredRatio * contentWidth) : midPoint; @@ -79,3 +49,49 @@ export class DiffEditorSash extends Disposable { return sashLeft; } } + +export class DiffEditorSash extends Disposable { + private readonly _sash = this._register(new Sash(this._domNode, { + getVerticalSashTop: (_sash: Sash): number => 0, + getVerticalSashLeft: (_sash: Sash): number => this.sashLeft.get(), + getVerticalSashHeight: (_sash: Sash): number => this._dimensions.height.get(), + }, { orientation: Orientation.VERTICAL })); + + private _startSashPosition: number | undefined = undefined; + + constructor( + private readonly _domNode: HTMLElement, + private readonly _dimensions: { height: IObservable; width: IObservable }, + private readonly _enabled: IObservable, + private readonly _boundarySashes: IObservable, + public readonly sashLeft: ISettableObservable, + private readonly _resetSash: () => void, + ) { + super(); + + this._register(this._sash.onDidStart(() => { + this._startSashPosition = this.sashLeft.get(); + })); + this._register(this._sash.onDidChange((e: ISashEvent) => { + this.sashLeft.set(this._startSashPosition! + (e.currentX - e.startX), undefined); + })); + this._register(this._sash.onDidEnd(() => this._sash.layout())); + this._register(this._sash.onDidReset(() => this._resetSash())); + + this._register(autorun(reader => { + const sashes = this._boundarySashes.read(reader); + if (sashes) { + this._sash.orthogonalEndSash = sashes.bottom; + } + })); + + this._register(autorun(reader => { + /** @description DiffEditorSash.layoutSash */ + const enabled = this._enabled.read(reader); + this._sash.state = enabled ? SashState.Enabled : SashState.Disabled; + this.sashLeft.read(reader); + this._dimensions.height.read(reader); + this._sash.layout(); + })); + } +} diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index 23a75bac47d..3b1b222de0d 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -30,6 +30,7 @@ import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewMod import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { DiffEditorOptions } from '../../diffEditorOptions'; +import { Range } from 'vs/editor/common/core/range'; /** * Ensures both editors have the same height by aligning unchanged lines. @@ -81,7 +82,7 @@ export class DiffEditorViewZones extends Disposable { })); const originalModelTokenizationCompleted = this._diffModel.map(m => - m ? observableFromEvent(m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined + m ? observableFromEvent(this, m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined ).map((m, reader) => m?.read(reader)); const alignments = derived((reader) => { @@ -187,7 +188,7 @@ export class DiffEditorViewZones extends Disposable { const renderOptions = RenderOptions.fromEditor(this._editors.modified); for (const a of alignmentsVal) { - if (a.diff && !renderSideBySide) { + if (a.diff && !renderSideBySide && (!this._options.useTrueInlineDiffRendering.read(reader) || !allowsTrueInlineDiffRendering(a.diff))) { if (!a.originalRange.isEmpty) { originalModelTokenizationCompleted.read(reader); // Update view-zones once tokenization completes @@ -525,13 +526,15 @@ function computeRangeAlignment( let lastModLineNumber = c.modified.startLineNumber; let lastOrigLineNumber = c.original.startLineNumber; - function emitAlignment(origLineNumberExclusive: number, modLineNumberExclusive: number) { + function emitAlignment(origLineNumberExclusive: number, modLineNumberExclusive: number, forceAlignment = false) { if (origLineNumberExclusive < lastOrigLineNumber || modLineNumberExclusive < lastModLineNumber) { return; } if (first) { first = false; - } else if (origLineNumberExclusive === lastOrigLineNumber || modLineNumberExclusive === lastModLineNumber) { + } else if (!forceAlignment && (origLineNumberExclusive === lastOrigLineNumber || modLineNumberExclusive === lastModLineNumber)) { + // This causes a re-alignment of an already aligned line. + // However, we don't care for the final alignment. return; } const originalRange = new LineRange(lastOrigLineNumber, origLineNumberExclusive); @@ -575,7 +578,7 @@ function computeRangeAlignment( } } - emitAlignment(c.original.endLineNumberExclusive, c.modified.endLineNumberExclusive); + emitAlignment(c.original.endLineNumberExclusive, c.modified.endLineNumberExclusive, true); lastOriginalLineNumber = c.original.endLineNumberExclusive; lastModifiedLineNumber = c.modified.endLineNumberExclusive; @@ -625,3 +628,17 @@ function getAdditionalLineHeights(editor: CodeEditorWidget, viewZonesToIgnore: R return result; } + +export function allowsTrueInlineDiffRendering(mapping: DetailedLineRangeMapping): boolean { + if (!mapping.innerChanges) { + return false; + } + return mapping.innerChanges.every(c => + (rangeIsSingleLine(c.modifiedRange) && rangeIsSingleLine(c.originalRange)) + || c.originalRange.equalsRange(new Range(1, 1, 1, 1)) + ); +} + +function rangeIsSingleLine(range: Range): boolean { + return range.startLineNumber === range.endLineNumber; +} diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index f5bbdb1b43f..ea3506b74ae 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -4,9 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { IObservable, ISettableObservable, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { derivedConstOnceDefined } from 'vs/base/common/observableInternal/utils'; import { Constants } from 'vs/base/common/uint'; +import { allowsTrueInlineDiffRendering } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones'; +import { DiffEditorViewModel, DiffState } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { diffEditorDefaultOptions } from 'vs/editor/common/config/diffEditor'; import { IDiffEditorBaseOptions, IDiffEditorOptions, IEditorOptions, ValidDiffEditorBaseOptions, clampedFloat, clampedInt, boolean as validateBooleanOption, stringSet as validateStringSetOption } from 'vs/editor/common/config/editorOptions'; +import { LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export class DiffEditorOptions { @@ -16,7 +20,7 @@ export class DiffEditorOptions { private readonly _diffEditorWidth = observableValue(this, 0); - private readonly _screenReaderMode = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + private readonly _screenReaderMode = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); constructor( options: Readonly, @@ -31,9 +35,16 @@ export class DiffEditorOptions { ); public readonly renderOverviewRuler = derived(this, reader => this._options.read(reader).renderOverviewRuler); - public readonly renderSideBySide = derived(this, reader => this._options.read(reader).renderSideBySide - && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader) && !this._screenReaderMode.read(reader)) - ); + public readonly renderSideBySide = derived(this, reader => { + if (this.compactMode.read(reader)) { + if (this.shouldRenderInlineViewInSmartMode.read(reader)) { + return false; + } + } + + return this._options.read(reader).renderSideBySide + && !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader) && !this._screenReaderMode.read(reader)); + }); public readonly readOnly = derived(this, reader => this._options.read(reader).readOnly); public readonly shouldRenderOldRevertArrows = derived(this, reader => { @@ -59,6 +70,14 @@ export class DiffEditorOptions { public readonly diffAlgorithm = derived(this, reader => this._options.read(reader).diffAlgorithm); public readonly showEmptyDecorations = derived(this, reader => this._options.read(reader).experimental.showEmptyDecorations!); public readonly onlyShowAccessibleDiffViewer = derived(this, reader => this._options.read(reader).onlyShowAccessibleDiffViewer); + public readonly compactMode = derived(this, reader => this._options.read(reader).compactMode); + private readonly trueInlineDiffRenderingEnabled: IObservable = derived(this, reader => + this._options.read(reader).experimental.useTrueInlineView! + ); + + public readonly useTrueInlineDiffRendering: IObservable = derived(this, reader => + !this.renderSideBySide.read(reader) && this.trueInlineDiffRenderingEnabled.read(reader) + ); public readonly hideUnchangedRegions = derived(this, reader => this._options.read(reader).hideUnchangedRegions.enabled!); public readonly hideUnchangedRegionsRevealLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.revealLineCount!); @@ -74,9 +93,37 @@ export class DiffEditorOptions { public setWidth(width: number): void { this._diffEditorWidth.set(width, undefined); } + + private readonly _model = observableValue(this, undefined); + + public setModel(model: DiffEditorViewModel | undefined) { + this._model.set(model, undefined); + } + + private readonly shouldRenderInlineViewInSmartMode = this._model + .map(this, model => derivedConstOnceDefined(this, reader => { + const diffs = model?.diff.read(reader); + return diffs ? isSimpleDiff(diffs, this.trueInlineDiffRenderingEnabled.read(reader)) : undefined; + })) + .flatten() + .map(this, v => !!v); + + public readonly inlineViewHideOriginalLineNumbers = this.compactMode; } -function validateDiffEditorOptions(options: Readonly, defaults: ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions { +function isSimpleDiff(diff: DiffState, supportsTrueDiffRendering: boolean): boolean { + return diff.mappings.every(m => isInsertion(m.lineRangeMapping) || isDeletion(m.lineRangeMapping) || (supportsTrueDiffRendering && allowsTrueInlineDiffRendering(m.lineRangeMapping))); +} + +function isInsertion(mapping: LineRangeMapping): boolean { + return mapping.original.length === 0; +} + +function isDeletion(mapping: LineRangeMapping): boolean { + return mapping.modified.length === 0; +} + +function validateDiffEditorOptions(options: Readonly, defaults: typeof diffEditorDefaultOptions | ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions { return { enableSplitViewResizing: validateBooleanOption(options.enableSplitViewResizing, defaults.enableSplitViewResizing), splitViewDefaultRatio: clampedFloat(options.splitViewDefaultRatio, 0.5, 0.1, 0.9), @@ -95,6 +142,7 @@ function validateDiffEditorOptions(options: Readonly, defaul experimental: { showMoves: validateBooleanOption(options.experimental?.showMoves, defaults.experimental.showMoves!), showEmptyDecorations: validateBooleanOption(options.experimental?.showEmptyDecorations, defaults.experimental.showEmptyDecorations!), + useTrueInlineView: validateBooleanOption(options.experimental?.useTrueInlineView, defaults.experimental.useTrueInlineView!), }, hideUnchangedRegions: { enabled: validateBooleanOption(options.hideUnchangedRegions?.enabled ?? (options.experimental as any)?.collapseUnchangedRegions, defaults.hideUnchangedRegions.enabled!), @@ -107,5 +155,6 @@ function validateDiffEditorOptions(options: Readonly, defaul renderSideBySideInlineBreakpoint: clampedInt(options.renderSideBySideInlineBreakpoint, defaults.renderSideBySideInlineBreakpoint, 0, Constants.MAX_SAFE_SMALL_INTEGER), useInlineViewWhenSpaceIsLimited: validateBooleanOption(options.useInlineViewWhenSpaceIsLimited, defaults.useInlineViewWhenSpaceIsLimited), renderGutterMenu: validateBooleanOption(options.renderGutterMenu, defaults.renderGutterMenu), + compactMode: validateBooleanOption(options.compactMode, defaults.compactMode), }; } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index bd065a313f7..557b757586c 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -8,7 +8,8 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, IReader, ISettableObservable, ITransaction, autorun, autorunWithStore, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from 'vs/base/common/observable'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; -import { filterWithPrevious, readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; +import { filterWithPrevious } from 'vs/editor/browser/widget/diffEditor/utils'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; import { ISerializedLineRange, LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { IDocumentDiff } from 'vs/editor/common/diff/documentDiffProvider'; @@ -298,7 +299,10 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo if (this._cancellationTokenSource.token.isCancellationRequested) { return; } - + if (model.original.isDisposed() || model.modified.isDisposed()) { + // TODO@hediet fishy? + return; + } result = normalizeDocumentDiff(result, model.original, model.modified); result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified) ?? result; result = applyModifiedEdits(result, modifiedTextEditInfos, model.original, model.modified) ?? result; @@ -390,6 +394,7 @@ function normalizeRangeMapping(rangeMapping: RangeMapping, original: ITextModel, let originalRange = rangeMapping.originalRange; let modifiedRange = rangeMapping.modifiedRange; if ( + originalRange.startColumn === 1 && modifiedRange.startColumn === 1 && (originalRange.endColumn !== 1 || modifiedRange.endColumn !== 1) && originalRange.endColumn === original.getLineMaxColumn(originalRange.endLineNumber) && modifiedRange.endColumn === modified.getLineMaxColumn(modifiedRange.endLineNumber) diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 7e5ac2ea408..e22b0291dd0 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -2,13 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, getWindow, h } from 'vs/base/browser/dom'; +import { getWindow, h } from 'vs/base/browser/dom'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { findLast } from 'vs/base/common/arraysFind'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, ITransaction, autorun, autorunWithStore, derived, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from 'vs/base/common/observable'; +import { IObservable, ITransaction, autorun, autorunWithStore, derived, disposableObservableValue, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from 'vs/base/common/observable'; import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; import 'vs/css!./style'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; @@ -19,13 +19,16 @@ import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { AccessibleDiffViewer, AccessibleDiffViewerModelFromEditors } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { DiffEditorDecorations } from 'vs/editor/browser/widget/diffEditor/components/diffEditorDecorations'; -import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditor/components/diffEditorSash'; +import { DiffEditorSash, SashLayout } from 'vs/editor/browser/widget/diffEditor/components/diffEditorSash'; import { DiffEditorViewZones } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones'; +import { DiffEditorGutter } from 'vs/editor/browser/widget/diffEditor/features/gutterFeature'; import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature'; import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; -import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, bindContextKey, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { CSSStyle, ObservableElementSizeObserver, RefCounted, applyStyle, applyViewZones, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; +import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; @@ -45,7 +48,6 @@ import { DiffEditorEditors } from './components/diffEditorEditors'; import { DelegatingEditor } from './delegatingEditorImpl'; import { DiffEditorOptions } from './diffEditorOptions'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel'; -import { DiffEditorGutter } from 'vs/editor/browser/widget/diffEditor/features/gutterFeature'; export interface IDiffCodeEditorWidgetOptions { originalEditor?: ICodeEditorWidgetOptions; @@ -56,26 +58,24 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { public static ENTIRE_DIFF_OVERVIEW_WIDTH = OverviewRulerFeature.ENTIRE_DIFF_OVERVIEW_WIDTH; private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [ - h('div.noModificationsOverlay@overlay', { style: { position: 'absolute', height: '100%', visibility: 'hidden', } }, [$('span', {}, 'No Changes')]), - h('div.editor.original@original', { style: { position: 'absolute', height: '100%', zIndex: '1', } }), - h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%', zIndex: '1', } }), + h('div.editor.original@original', { style: { position: 'absolute', height: '100%', } }), + h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%', } }), h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }), ]); - private readonly _diffModel = observableValue(this, undefined); - private _shouldDisposeDiffModel = false; + private readonly _diffModelSrc = this._register(disposableObservableValue | undefined>(this, undefined)); + private readonly _diffModel = derived(this, reader => this._diffModelSrc.read(reader)?.object); public readonly onDidChangeModel = Event.fromObservableLight(this._diffModel); public get onDidContentSizeChange() { return this._editors.onDidContentSizeChange; } private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._domElement)); - private readonly _instantiationService = this._parentInstantiationService.createChild( + private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( new ServiceCollection([IContextKeyService, this._contextKeyService]) - ); + )); private readonly _rootSizeObserver: ObservableElementSizeObserver; - /** - * Is undefined if and only if side-by-side - */ + + private readonly _sashLayout: SashLayout; private readonly _sash: IObservable; private readonly _boundarySashes = observableValue(this, undefined); @@ -112,7 +112,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._contextKeyService.createKey('isInDiffEditor', true); this._domElement.appendChild(this.elements.root); - this._register(toDisposable(() => this._domElement.removeChild(this.elements.root))); + this._register(toDisposable(() => this.elements.root.remove())); this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension)); this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false); @@ -176,17 +176,23 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { ) ).recomputeInitiallyAndOnChange(this._store); + const dimensions = { + height: this._rootSizeObserver.height, + width: this._rootSizeObserver.width.map((w, reader) => w - (this._overviewRulerPart.read(reader)?.width ?? 0)), + }; + + this._sashLayout = new SashLayout(this._options, dimensions); + this._sash = derivedDisposable(this, reader => { const showSash = this._options.renderSideBySide.read(reader); this.elements.root.classList.toggle('side-by-side', showSash); return !showSash ? undefined : new DiffEditorSash( - this._options, this.elements.root, - { - height: this._rootSizeObserver.height, - width: this._rootSizeObserver.width.map((w, reader) => w - (this._overviewRulerPart.read(reader)?.width ?? 0)), - }, + dimensions, + this._options.enableSplitViewResizing, this._boundarySashes, + this._sashLayout.sashLeft, + () => this._sashLayout.resetSash(), ); }).recomputeInitiallyAndOnChange(this._store); @@ -273,7 +279,10 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { readHotReloadableExport(DiffEditorGutter, reader), this.elements.root, this._diffModel, - this._editors + this._editors, + this._options, + this._sashLayout, + this._boundarySashes, ) : undefined; }); @@ -293,17 +302,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._movedBlocksLinesPart.set(value, undefined); }); - this._register(applyStyle(this.elements.overlay, { - width: this._layoutInfo.map((i, r) => i.originalEditor.width + (this._options.renderSideBySide.read(r) ? 0 : i.modifiedEditor.width)), - visibility: derived(reader => /** @description visibility */(this._options.hideUnchangedRegions.read(reader) && this._diffModel.read(reader)?.diff.read(reader)?.mappings.length === 0) - ? 'visible' : 'hidden' - ), - })); - this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, true))); this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, false))); - const isInitializingDiff = this._diffModel.map(this, (m, reader) => { /** @isInitializingDiff isDiffUpToDate */ if (!m) { return undefined; } @@ -317,14 +318,23 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } })); - this._register(toDisposable(() => { - if (this._shouldDisposeDiffModel) { - this._diffModel.get()?.dispose(); - } + this._register(autorunWithStore((reader, store) => { + store.add(new (readHotReloadableExport(RevertButtonsFeature, reader))(this._editors, this._diffModel, this._options, this)); })); this._register(autorunWithStore((reader, store) => { - store.add(new (readHotReloadableExport(RevertButtonsFeature, reader))(this._editors, this._diffModel, this._options, this)); + const model = this._diffModel.read(reader); + if (!model) { return; } + for (const m of [model.model.original, model.model.modified]) { + store.add(m.onWillDispose(e => { + onUnexpectedError(new BugIndicatingError('TextModel got disposed before DiffEditorWidget model got reset')); + this.setModel(null); + })); + } + })); + + this._register(autorun(reader => { + this._options.setModel(this._diffModel.read(reader)); })); } @@ -345,6 +355,12 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { const fullWidth = this._rootSizeObserver.width.read(reader); const fullHeight = this._rootSizeObserver.height.read(reader); + if (this._rootSizeObserver.automaticLayout) { + this.elements.root.style.height = '100%'; + } else { + this.elements.root.style.height = fullHeight + 'px'; + } + const sash = this._sash.read(reader); const gutter = this._gutter.read(reader); @@ -369,8 +385,13 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } else { gutterLeft = 0; + const shouldHideOriginalLineNumbers = this._options.inlineViewHideOriginalLineNumbers.read(reader); originalLeft = gutterWidth; - originalWidth = Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft); + if (shouldHideOriginalLineNumbers) { + originalWidth = 0; + } else { + originalWidth = Math.max(5, this._editors.originalObs.layoutInfoDecorationsLeft.read(reader)); + } modifiedLeft = gutterWidth + originalWidth; modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth; @@ -456,30 +477,36 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { override getModel(): IDiffEditorModel | null { return this._diffModel.get()?.model ?? null; } - override setModel(model: IDiffEditorModel | null | IDiffEditorViewModel, tx?: ITransaction): void { - if (!model && this._diffModel.get()) { + override setModel(model: IDiffEditorModel | null | IDiffEditorViewModel): void { + const vm = !model ? null + : ('model' in model) ? RefCounted.create(model).createNewRef(this) + : RefCounted.create(this.createViewModel(model), this); + this.setDiffModel(vm); + } + + setDiffModel(viewModel: RefCounted | null, tx?: ITransaction): void { + const currentModel = this._diffModel.get(); + + if (!viewModel && currentModel) { // Transitioning from a model to no-model this._accessibleDiffViewer.get().close(); } - const vm = model ? ('model' in model) ? { model, shouldDispose: false } : { model: this.createViewModel(model), shouldDispose: true } : undefined; - - if (this._diffModel.get() !== vm?.model) { + if (this._diffModel.get() !== viewModel?.object) { subtransaction(tx, tx => { + const vm = viewModel?.object; /** @description DiffEditorWidget.setModel */ observableFromEvent.batchEventsGlobally(tx, () => { - this._editors.original.setModel(vm ? vm.model.model.original : null); - this._editors.modified.setModel(vm ? vm.model.model.modified : null); + this._editors.original.setModel(vm ? vm.model.original : null); + this._editors.modified.setModel(vm ? vm.model.modified : null); }); - const prevValue = this._diffModel.get(); - const shouldDispose = this._shouldDisposeDiffModel; - - this._shouldDisposeDiffModel = vm?.shouldDispose ?? false; - this._diffModel.set(vm?.model as (DiffEditorViewModel | undefined), tx); - - if (shouldDispose) { - prevValue?.dispose(); - } + const prevValueRef = this._diffModelSrc.get()?.createNewRef(this); + this._diffModelSrc.set(viewModel?.createNewRef(this) as RefCounted | undefined, tx); + setTimeout(() => { + // async, so that this runs after the transaction finished. + // TODO: use the transaction to schedule disposal + prevValueRef?.dispose(); + }, 0); }); } } diff --git a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts index d9a4c6317df..19cee5997cc 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts @@ -67,6 +67,16 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I return this.diffAlgorithm.computeDiff(original, modified, options, cancellationToken); } + if (original.isDisposed() || modified.isDisposed()) { + // TODO@hediet + return { + changes: [], + identical: true, + quitEarly: false, + moves: [], + }; + } + // This significantly speeds up the case when the original file is empty if (original.getLineCount() === 1 && original.getLineMaxColumn(1) === 1) { if (modified.getLineCount() === 1 && modified.getLineMaxColumn(1) === 1) { @@ -115,9 +125,9 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I }, { owner: 'hediet'; - timeMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'To understand if the new diff algorithm is slower/faster than the old one' }; - timedOut: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'To understand how often the new diff algorithm times out' }; - detectedMoves: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'To understand how often the new diff algorithm detects moves' }; + timeMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'To understand if the new diff algorithm is slower/faster than the old one' }; + timedOut: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'To understand how often the new diff algorithm times out' }; + detectedMoves: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'To understand how often the new diff algorithm detects moves' }; comment: 'This event gives insight about the performance of the new diff algorithm.'; }>('diffEditor.computeDiff', { @@ -142,7 +152,7 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I // max 10 items in cache if (WorkerBasedDocumentDiffProvider.diffCache.size > 10) { - WorkerBasedDocumentDiffProvider.diffCache.delete(WorkerBasedDocumentDiffProvider.diffCache.keys().next().value); + WorkerBasedDocumentDiffProvider.diffCache.delete(WorkerBasedDocumentDiffProvider.diffCache.keys().next().value!); } WorkerBasedDocumentDiffProvider.diffCache.set(uriKey, { result, context }); diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index 8410b6eb85b..010fba5ec96 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -7,19 +7,23 @@ import { EventType, addDisposableListener, h } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, autorun, autorunWithStore, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { derivedDisposable, derivedWithSetter } from 'vs/base/common/observableInternal/derived'; import { URI } from 'vs/base/common/uri'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; +import { DiffEditorSash, SashLayout } from 'vs/editor/browser/widget/diffEditor/components/diffEditorSash'; +import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; -import { appendRemoveOnDispose, applyStyle } from 'vs/editor/browser/widget/diffEditor/utils'; +import { appendRemoveOnDispose, applyStyle, prependRemoveOnDispose } from 'vs/editor/browser/widget/diffEditor/utils'; import { EditorGutter, IGutterItemInfo, IGutterItemView } from 'vs/editor/browser/widget/diffEditor/utils/editorGutter'; import { ActionRunnerWithContext } from 'vs/editor/browser/widget/multiDiffEditor/utils'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Range } from 'vs/editor/common/core/range'; -import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { TextEdit } from 'vs/editor/common/core/textEdit'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { TextModelText } from 'vs/editor/common/model/textModelText'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -33,24 +37,28 @@ const width = 35; export class DiffEditorGutter extends Disposable { private readonly _menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService)); - private readonly _actions = observableFromEvent(this._menu.onDidChange, () => this._menu.getActions()); + private readonly _actions = observableFromEvent(this, this._menu.onDidChange, () => this._menu.getActions()); private readonly _hasActions = this._actions.map(a => a.length > 0); + private readonly _showSash = derived(this, reader => this._options.renderSideBySide.read(reader) && this._hasActions.read(reader)); public readonly width = derived(this, reader => this._hasActions.read(reader) ? width : 0); - private readonly elements = h('div.gutter@gutter', { style: { position: 'absolute', height: '100%', width: width + 'px', zIndex: '0' } }, []); + private readonly elements = h('div.gutter@gutter', { style: { position: 'absolute', height: '100%', width: width + 'px' } }, []); constructor( diffEditorRoot: HTMLDivElement, private readonly _diffModel: IObservable, private readonly _editors: DiffEditorEditors, + private readonly _options: DiffEditorOptions, + private readonly _sashLayout: SashLayout, + private readonly _boundarySashes: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IMenuService private readonly _menuService: IMenuService, ) { super(); - this._register(appendRemoveOnDispose(diffEditorRoot, this.elements.root)); + this._register(prependRemoveOnDispose(diffEditorRoot, this.elements.root)); this._register(addDisposableListener(this.elements.root, 'click', () => { this._editors.modified.focus(); @@ -58,6 +66,21 @@ export class DiffEditorGutter extends Disposable { this._register(applyStyle(this.elements.root, { display: this._hasActions.map(a => a ? 'block' : 'none') })); + derivedDisposable(this, reader => { + const showSash = this._showSash.read(reader); + return !showSash ? undefined : new DiffEditorSash( + diffEditorRoot, + this._sashLayout.dimensions, + this._options.enableSplitViewResizing, + this._boundarySashes, + derivedWithSetter( + this, reader => this._sashLayout.sashLeft.read(reader) - width, + (v, tx) => this._sashLayout.sashLeft.set(v + width, tx) + ), + () => this._sashLayout.resetSash(), + ); + }).recomputeInitiallyAndOnChange(this._store); + this._register(new EditorGutter(this._editors.modified, this.elements.root, { getIntersectingGutterItems: (range, reader) => { const model = this._diffModel.read(reader); @@ -106,8 +129,11 @@ export class DiffEditorGutter extends Disposable { public computeStagedValue(mapping: DetailedLineRangeMapping): string { const c = mapping.innerChanges ?? []; - const edit = new TextEdit(c.map(c => new SingleTextEdit(c.originalRange, this._editors.modifiedModel.get()!.getValueInRange(c.modifiedRange)))); - const value = edit.apply(new TextModelText(this._editors.original.getModel()!)); + const modified = new TextModelText(this._editors.modifiedModel.get()!); + const original = new TextModelText(this._editors.original.getModel()!); + + const edit = new TextEdit(c.map(c => c.toTextEdit(modified))); + const value = edit.apply(original); return value; } @@ -252,9 +278,6 @@ class DiffToolBar extends Disposable implements IGutterItemView { // Item might have changed itemHeight = this._elements.buttons.clientHeight; - this._elements.root.style.top = itemRange.start + 'px'; - this._elements.root.style.height = itemRange.length + 'px'; - const middleHeight = itemRange.length / 2 - itemHeight / 2; const margin = itemHeight; diff --git a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts index 248f2b46d81..336bdb4304d 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts @@ -8,11 +8,12 @@ import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/i import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, autorun, derived, derivedWithStore, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, autorun, derived, derivedWithStore, observableValue, transaction } from 'vs/base/common/observable'; import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; import { ThemeIcon } from 'vs/base/common/themables'; import { isDefined } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel, RevealPreference, UnchangedRegion } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; @@ -31,7 +32,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti * Make sure to add the view zones to the editor! */ export class HideUnchangedRegionsFeature extends Disposable { - private static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource) | undefined>('breadcrumbsSourceFactory', undefined); + private static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource)>( + HideUnchangedRegionsFeature, () => ({ + dispose() { + }, + getBreadcrumbItems(startRange, reader) { + return []; + }, + })); public static setBreadcrumbsSourceFactory(factory: (textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource) { this._breadcrumbsSourceFactory.set(factory, undefined); } @@ -97,41 +105,72 @@ export class HideUnchangedRegionsFeature extends Disposable { const modViewZones: IObservableViewZone[] = []; const sideBySide = this._options.renderSideBySide.read(reader); + const compactMode = this._options.compactMode.read(reader); + const curUnchangedRegions = unchangedRegions.read(reader); - for (const r of curUnchangedRegions) { + for (let i = 0; i < curUnchangedRegions.length; i++) { + const r = curUnchangedRegions[i]; if (r.shouldHideControls(reader)) { continue; } - { - const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); - const origVz = new PlaceholderViewZone(d, 24); - origViewZones.push(origVz); - store.add(new CollapsedCodeOverlayWidget( - this._editors.original, - origVz, - r, - r.originalUnchangedRange, - !sideBySide, - modifiedOutlineSource, - l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), - this._options, - )); + if (compactMode && (i === 0 || i === curUnchangedRegions.length - 1)) { + continue; } - { - const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); - const modViewZone = new PlaceholderViewZone(d, 24); - modViewZones.push(modViewZone); - store.add(new CollapsedCodeOverlayWidget( - this._editors.modified, - modViewZone, - r, - r.modifiedUnchangedRange, - false, - modifiedOutlineSource, - l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), - this._options, - )); + + if (compactMode) { + { + const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); + const origVz = new PlaceholderViewZone(d, 12); + origViewZones.push(origVz); + store.add(new CompactCollapsedCodeOverlayWidget( + this._editors.original, + origVz, + r, + !sideBySide, + )); + } + { + const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); + const modViewZone = new PlaceholderViewZone(d, 12); + modViewZones.push(modViewZone); + store.add(new CompactCollapsedCodeOverlayWidget( + this._editors.modified, + modViewZone, + r, + )); + } + } else { + { + const d = derived(this, reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); + const origVz = new PlaceholderViewZone(d, 24); + origViewZones.push(origVz); + store.add(new CollapsedCodeOverlayWidget( + this._editors.original, + origVz, + r, + r.originalUnchangedRange, + !sideBySide, + modifiedOutlineSource, + l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), + this._options, + )); + } + { + const d = derived(this, reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); + const modViewZone = new PlaceholderViewZone(d, 24); + modViewZones.push(modViewZone); + store.add(new CollapsedCodeOverlayWidget( + this._editors.modified, + modViewZone, + r, + r.modifiedUnchangedRange, + false, + modifiedOutlineSource, + l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, RevealPreference.FromBottom, undefined), + this._options, + )); + } } } @@ -228,6 +267,39 @@ export class HideUnchangedRegionsFeature extends Disposable { } } +class CompactCollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { + private readonly _nodes = h('div.diff-hidden-lines-compact', [ + h('div.line-left', []), + h('div.text@text', []), + h('div.line-right', []) + ]); + + constructor( + editor: ICodeEditor, + _viewZone: PlaceholderViewZone, + private readonly _unchangedRegion: UnchangedRegion, + private readonly _hide: boolean = false, + ) { + const root = h('div.diff-hidden-lines-widget'); + super(editor, _viewZone, root.root); + root.root.appendChild(this._nodes.root); + + if (this._hide) { + this._nodes.root.replaceChildren(); + } + + this._register(autorun(reader => { + /** @description update labels */ + + if (!this._hide) { + const lineCount = this._unchangedRegion.getHiddenModifiedRange(reader).length; + const linesHiddenText = localize('hiddenLines', '{0} hidden lines', lineCount); + this._nodes.text.innerText = linesHiddenText; + } + })); + } +} + class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { private readonly _nodes = h('div.diff-hidden-lines', [ h('div.top@top', { title: localize('diff.hiddenLines.top', 'Click or drag to show more above') }), @@ -255,12 +327,8 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { super(_editor, _viewZone, root.root); root.root.appendChild(this._nodes.root); - const layoutInfo = observableFromEvent(this._editor.onDidLayoutChange, () => - this._editor.getLayoutInfo() - ); - if (!this._hide) { - this._register(applyStyle(this._nodes.first, { width: layoutInfo.map((l) => l.contentLeft) })); + this._register(applyStyle(this._nodes.first, { width: observableCodeEditor(this._editor).layoutInfoContentLeft })); } else { reset(this._nodes.first); } diff --git a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts index af2b6a88995..afb29693f81 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts @@ -7,7 +7,7 @@ import { h } from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; import { booleanComparator, compareBy, numberComparator, tieBreakComparators } from 'vs/base/common/arrays'; -import { findMaxIdxBy } from 'vs/base/common/arraysFind'; +import { findMaxIdx } from 'vs/base/common/arraysFind'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, autorun, autorunHandleChanges, autorunWithStore, constObservable, derived, derivedWithStore, observableFromEvent, observableSignalFromEvent, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable'; @@ -26,8 +26,8 @@ export class MovedBlocksLinesFeature extends Disposable { public static readonly movedCodeBlockPadding = 4; private readonly _element: SVGElement; - private readonly _originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); - private readonly _modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); + private readonly _originalScrollTop = observableFromEvent(this, this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); + private readonly _modifiedScrollTop = observableFromEvent(this, this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); private readonly _viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); public readonly width = observableValue(this, 0); @@ -258,7 +258,7 @@ class LinesLayout { if (trackIdx === -1) { const maxTrackCount = 6; if (setsPerTrack.length >= maxTrackCount) { - trackIdx = findMaxIdxBy(setsPerTrack, compareBy(set => set.intersectWithRangeLength(line), numberComparator)); + trackIdx = findMaxIdx(setsPerTrack, compareBy(set => set.intersectWithRangeLength(line), numberComparator)); } else { trackIdx = setsPerTrack.length; setsPerTrack.push(new OffsetRangeSet()); diff --git a/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts index 8141cd9452c..017d8268f6e 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts @@ -23,7 +23,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; export class OverviewRulerFeature extends Disposable { private static readonly ONE_OVERVIEW_WIDTH = 15; - public static readonly ENTIRE_DIFF_OVERVIEW_WIDTH = OverviewRulerFeature.ONE_OVERVIEW_WIDTH * 2; + public static readonly ENTIRE_DIFF_OVERVIEW_WIDTH = this.ONE_OVERVIEW_WIDTH * 2; public readonly width = OverviewRulerFeature.ENTIRE_DIFF_OVERVIEW_WIDTH; constructor( diff --git a/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts b/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts index 36bd4d465ff..80b553bcb30 100644 --- a/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts @@ -12,13 +12,13 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export const diffMoveBorder = registerColor( 'diffEditor.move.border', - { dark: '#8b8b8b9c', light: '#8b8b8b9c', hcDark: '#8b8b8b9c', hcLight: '#8b8b8b9c', }, + '#8b8b8b9c', localize('diffEditor.move.border', 'The border color for text that got moved in the diff editor.') ); export const diffMoveBorderActive = registerColor( 'diffEditor.moveActive.border', - { dark: '#FFA500', light: '#FFA500', hcDark: '#FFA500', hcLight: '#FFA500', }, + '#FFA500', localize('diffEditor.moveActive.border', 'The active border color for text that got moved in the diff editor.') ); diff --git a/src/vs/editor/browser/widget/diffEditor/style.css b/src/vs/editor/browser/widget/diffEditor/style.css index 49ad115e36b..4489d84be38 100644 --- a/src/vs/editor/browser/widget/diffEditor/style.css +++ b/src/vs/editor/browser/widget/diffEditor/style.css @@ -276,10 +276,14 @@ background-color: var(--vscode-diffEditorGutter-insertedLineBackground, var(--vscode-diffEditor-insertedLineBackground), var(--vscode-diffEditor-insertedTextBackground)); } -.monaco-editor .char-delete, .monaco-diff-editor .char-delete { +.monaco-editor .char-delete, .monaco-diff-editor .char-delete, .monaco-editor .inline-deleted-text { background-color: var(--vscode-diffEditor-removedTextBackground); } +.monaco-editor .inline-deleted-text { + text-decoration: line-through; +} + .monaco-editor .line-delete, .monaco-diff-editor .line-delete { background-color: var(--vscode-diffEditor-removedLineBackground, var(--vscode-diffEditor-removedTextBackground)); } @@ -328,6 +332,10 @@ flex-shrink: 0; flex-grow: 0; + & > div { + position: absolute; + } + .gutterItem { opacity: 0; transition: opacity 0.7s; @@ -374,8 +382,7 @@ .actions-container { width: fit-content; border-radius: 4px; - border: 1px var(--vscode-menu-border) solid; - background: var(--vscode-editor-background); + background: var(--vscode-editorGutter-commentRangeForeground); .action-item { &:hover { @@ -383,7 +390,7 @@ } .action-label { - padding: 0.5px 1px; + padding: 1px 2px; } } } @@ -392,3 +399,29 @@ } } } + + +.monaco-diff-editor .diff-hidden-lines-compact { + display: flex; + height: 11px; + .line-left, .line-right { + height: 1px; + border-top: 1px solid; + border-color: var(--vscode-editorCodeLens-foreground); + opacity: 0.5; + margin: auto; + width: 100%; + } + + .line-left { + width: 20px; + } + + .text { + color: var(--vscode-editorCodeLens-foreground); + text-wrap: nowrap; + font-size: 11px; + line-height: 11px; + margin: 0 4px; + } +} diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index a1e263948f2..ed24ac50430 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -6,9 +6,8 @@ import { IDimension } from 'vs/base/browser/dom'; import { findLast } from 'vs/base/common/arraysFind'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from 'vs/base/common/observable'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ICodeEditor, IOverlayWidget, IViewZone } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; @@ -16,8 +15,6 @@ import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { TextLength } from 'vs/editor/common/core/textLength'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export function joinCombine(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] { if (arr1.length === 0) { @@ -78,19 +75,15 @@ export function applyObservableDecorations(editor: ICodeEditor, decorations: IOb export function appendRemoveOnDispose(parent: HTMLElement, child: HTMLElement) { parent.appendChild(child); return toDisposable(() => { - parent.removeChild(child); + child.remove(); }); } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); +export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) { + parent.prepend(child); + return toDisposable(() => { + child.remove(); + }); } export class ObservableElementSizeObserver extends Disposable { @@ -102,6 +95,9 @@ export class ObservableElementSizeObserver extends Disposable { private readonly _height: ISettableObservable; public get height(): IObservable { return this._height; } + private _automaticLayout: boolean = false; + public get automaticLayout(): boolean { return this._automaticLayout; } + constructor(element: HTMLElement | null, dimension: IDimension | undefined) { super(); @@ -121,6 +117,7 @@ export class ObservableElementSizeObserver extends Disposable { } public setAutomaticLayout(automaticLayout: boolean): void { + this._automaticLayout = automaticLayout; if (automaticLayout) { this.elementSizeObserver.startObserving(); } else { @@ -300,29 +297,6 @@ export function applyStyle(domNode: HTMLElement, style: Partial<{ [TKey in keyof }); } -export function readHotReloadableExport(value: T, reader: IReader | undefined): T { - observeHotReloadableExports([value], reader); - return value; -} - -export function observeHotReloadableExports(values: any[], reader: IReader | undefined): void { - if (isHotReloadEnabled()) { - const o = observableSignalFromEvent( - 'reload', - event => registerHotReloadHandler(({ oldExports }) => { - if (![...Object.values(oldExports)].some(v => values.includes(v))) { - return undefined; - } - return (_newExports) => { - event(undefined); - return true; - }; - }) - ); - o.read(reader); - } -} - export function applyViewZones(editor: ICodeEditor, viewZones: IObservable, setIsUpdating?: (isUpdatingViewZones: boolean) => void, zoneIds?: Set): IDisposable { const store = new DisposableStore(); const lastViewZoneIds: string[] = []; @@ -433,13 +407,6 @@ function lengthBetweenPositions(position1: Position, position2: Position): TextL } } -export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { - const boundKey = key.bindTo(service); - return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { - boundKey.set(computeValue(reader)); - }); -} - export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] { let prev: T | undefined; return arr.filter(cur => { @@ -448,3 +415,104 @@ export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undef return result; }); } + +export interface IRefCounted extends IDisposable { + createNewRef(): this; +} + +export abstract class RefCounted implements IDisposable, IReference { + public static create(value: T, debugOwner: object | undefined = undefined): RefCounted { + return new BaseRefCounted(value, value, debugOwner); + } + + public static createWithDisposable(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted { + const store = new DisposableStore(); + store.add(disposable); + store.add(value); + return new BaseRefCounted(value, store, debugOwner); + } + + public static createOfNonDisposable(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted { + return new BaseRefCounted(value, disposable, debugOwner); + } + + public abstract createNewRef(debugOwner?: object | undefined): RefCounted; + + public abstract dispose(): void; + + public abstract get object(): T; +} + +class BaseRefCounted extends RefCounted { + private _refCount = 1; + private _isDisposed = false; + private readonly _owners: object[] = []; + + constructor( + public override readonly object: T, + private readonly _disposable: IDisposable, + private readonly _debugOwner: object | undefined, + ) { + super(); + + if (_debugOwner) { + this._addOwner(_debugOwner); + } + } + + private _addOwner(debugOwner: object | undefined) { + if (debugOwner) { + this._owners.push(debugOwner); + } + } + + public createNewRef(debugOwner?: object | undefined): RefCounted { + this._refCount++; + if (debugOwner) { + this._addOwner(debugOwner); + } + return new ClonedRefCounted(this, debugOwner); + } + + public dispose(): void { + if (this._isDisposed) { return; } + this._isDisposed = true; + this._decreaseRefCount(this._debugOwner); + } + + public _decreaseRefCount(debugOwner?: object | undefined): void { + this._refCount--; + if (this._refCount === 0) { + this._disposable.dispose(); + } + + if (debugOwner) { + const idx = this._owners.indexOf(debugOwner); + if (idx !== -1) { + this._owners.splice(idx, 1); + } + } + } +} + +class ClonedRefCounted extends RefCounted { + private _isDisposed = false; + constructor( + private readonly _base: BaseRefCounted, + private readonly _debugOwner: object | undefined, + ) { + super(); + } + + public get object(): T { return this._base.object; } + + public createNewRef(debugOwner?: object | undefined): RefCounted { + return this._base.createNewRef(debugOwner); + } + + public dispose(): void { + if (this._isDisposed) { return; } + this._isDisposed = true; + this._base._decreaseRefCount(this._debugOwner); + } +} diff --git a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts index 1c3341a73ef..a301fc6124b 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts @@ -11,12 +11,12 @@ import { LineRange } from 'vs/editor/common/core/lineRange'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; export class EditorGutter extends Disposable { - private readonly scrollTop = observableFromEvent( + private readonly scrollTop = observableFromEvent(this, this._editor.onDidScrollChange, (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() ); private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); - private readonly modelAttached = observableFromEvent( + private readonly modelAttached = observableFromEvent(this, this._editor.onDidChangeModel, (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() ); @@ -136,7 +136,7 @@ export class EditorGutter extends D for (const id of unusedIds) { const view = this.views.get(id)!; view.gutterItemView.dispose(); - this._domNode.removeChild(view.domNode); + view.domNode.remove(); this.views.delete(id); } } diff --git a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts index d58781aabfe..c9e2cca5fb6 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground } from 'vs/platform/theme/common/colorRegistry'; export const multiDiffEditorHeaderBackground = registerColor( 'multiDiffEditor.headerBackground', @@ -14,7 +14,7 @@ export const multiDiffEditorHeaderBackground = registerColor( export const multiDiffEditorBackground = registerColor( 'multiDiffEditor.background', - { dark: 'editorBackground', light: 'editorBackground', hcDark: 'editorBackground', hcLight: 'editorBackground', }, + editorBackground, localize('multiDiffEditor.background', 'The background color of the multi file diff editor') ); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index c6f3ca9de9e..8f7d4a12ab0 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -6,20 +6,20 @@ import { h } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { autorun, derived, observableFromEvent } from 'vs/base/common/observable'; -import { IObservable, globalTransaction, observableValue } from 'vs/base/common/observableInternal/base'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { autorun, derived } from 'vs/base/common/observable'; +import { globalTransaction, observableValue } from 'vs/base/common/observableInternal/base'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { DocumentDiffItemViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; import { IWorkbenchUIElementFactory } from 'vs/editor/browser/widget/multiDiffEditor/workbenchUIElementFactory'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IObjectData, IPooledObject } from './objectPool'; import { ActionRunnerWithContext } from './utils'; -import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export class TemplateData implements IObjectData { constructor( @@ -81,8 +81,8 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< overflowWidgetsDomNode: this._overflowWidgetsDomNode, }, {})); - private readonly isModifedFocused = isFocused(this.editor.getModifiedEditor()); - private readonly isOriginalFocused = isFocused(this.editor.getOriginalEditor()); + private readonly isModifedFocused = observableCodeEditor(this.editor.getModifiedEditor()).isFocused; + private readonly isOriginalFocused = observableCodeEditor(this.editor.getOriginalEditor()).isFocused; public readonly isFocused = derived(this, reader => this.isModifedFocused.read(reader) || this.isOriginalFocused.read(reader)); private readonly _resourceLabel = this._workbenchUIElementFactory.createResourceLabel @@ -148,8 +148,8 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< })); this._register(autorun(reader => { - const isFocused = this.isFocused.read(reader); - this._elements.root.classList.toggle('focused', isFocused); + const isActive = this._viewModel.read(reader)?.isActive.read(reader); + this._elements.root.classList.toggle('active', isActive); })); this._container.appendChild(this._elements.root); @@ -177,7 +177,7 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< private _data: TemplateData | undefined; - public setData(data: TemplateData): void { + public setData(data: TemplateData | undefined): void { this._data = data; function updateOptions(options: IDiffEditorOptions): IDiffEditorOptions { return { @@ -198,13 +198,17 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< }; } - const value = data.viewModel.entry.value!; // TODO - - if (value.onOptionsDidChange) { - this._dataStore.add(value.onOptionsDidChange(() => { - this.editor.updateOptions(updateOptions(value.options ?? {})); - })); + if (!data) { + globalTransaction(tx => { + this._viewModel.set(undefined, tx); + this.editor.setDiffModel(null, tx); + this._dataStore.clear(); + }); + return; } + + const value = data.viewModel.documentDiffItem; + globalTransaction(tx => { this._resourceLabel?.setUri(data.viewModel.modifiedUri ?? data.viewModel.originalUri!, { strikethrough: data.viewModel.modifiedUri === undefined }); @@ -231,9 +235,19 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< this._dataStore.clear(); this._viewModel.set(data.viewModel, tx); - this.editor.setModel(data.viewModel.diffEditorViewModel, tx); + this.editor.setDiffModel(data.viewModel.diffEditorViewModelRef, tx); this.editor.updateOptions(updateOptions(value.options ?? {})); }); + if (value.onOptionsDidChange) { + this._dataStore.add(value.onOptionsDidChange(() => { + this.editor.updateOptions(updateOptions(value.options ?? {})); + })); + } + data.viewModel.isAlive.recomputeInitiallyAndOnChange(this._dataStore, value => { + if (!value) { + this.setData(undefined); + } + }); } private readonly _headerHeight = /*this._elements.header.clientHeight*/ 40; @@ -276,15 +290,3 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< this._elements.root.style.visibility = 'hidden'; // Some editor parts are still visible } } - -function isFocused(editor: ICodeEditor): IObservable { - return observableFromEvent( - h => { - const store = new DisposableStore(); - store.add(editor.onDidFocusEditorWidget(() => h(true))); - store.add(editor.onDidBlurEditorWidget(() => h(false))); - return store; - }, - () => editor.hasTextFocus() - ); -} diff --git a/src/vs/editor/browser/widget/multiDiffEditor/model.ts b/src/vs/editor/browser/widget/multiDiffEditor/model.ts index 02f37e47ad2..f2b11dc998c 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/model.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/model.ts @@ -3,39 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; +import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; +import { RefCounted } from 'vs/editor/browser/widget/diffEditor/utils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITextModel } from 'vs/editor/common/model'; import { ContextKeyValue } from 'vs/platform/contextkey/common/contextkey'; export interface IMultiDiffEditorModel { - readonly documents: readonly LazyPromise[]; - readonly onDidChange: Event; + readonly documents: IValueWithChangeEvent[]>; readonly contextKeys?: Record; } -export interface LazyPromise { - request(): Promise; - readonly value: T | undefined; - readonly onHasValueDidChange: Event; -} - -export class ConstLazyPromise implements LazyPromise { - public readonly onHasValueDidChange = Event.None; - - constructor( - private readonly _value: T - ) { } - - public request(): Promise { - return Promise.resolve(this._value); - } - - public get value(): T { - return this._value; - } -} - export interface IDocumentDiffItem { /** * undefined if the file was created. diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 1fec3c57c96..30924647869 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -3,27 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; -import { mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ITransaction, derived, observableValue, transaction } from 'vs/base/common/observable'; +import { constObservable, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent } from 'vs/base/common/observableInternal/utils'; +import { URI } from 'vs/base/common/uri'; import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorOptions'; import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; -import { IDocumentDiffItem, IMultiDiffEditorModel, LazyPromise } from 'vs/editor/browser/widget/multiDiffEditor/model'; +import { RefCounted } from 'vs/editor/browser/widget/diffEditor/utils'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Selection } from 'vs/editor/common/core/selection'; import { IDiffEditorViewModel } from 'vs/editor/common/editorCommon'; import { IModelService } from 'vs/editor/common/services/model'; import { ContextKeyValue } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { URI } from 'vs/base/common/uri'; export class MultiDiffEditorViewModel extends Disposable { - private readonly _documents = observableFromEvent(this.model.onDidChange, /** @description MultiDiffEditorViewModel.documents */() => this.model.documents); + private readonly _documents: IObservable[]> = observableFromValueWithChangeEvent(this.model, this.model.documents); - public readonly items = mapObservableArrayCached(this, this._documents, (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d))) + public readonly items: IObservable = mapObservableArrayCached( + this, + this._documents, + (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this)) + ) .recomputeInitiallyAndOnChange(this._store); - public readonly activeDiffItem = observableValue(this, undefined); + public readonly focusedDiffItem = derived(this, reader => this.items.read(reader).find(i => i.isFocused.read(reader))); + public readonly activeDiffItem = derivedObservableWithWritableCache(this, (reader, lastValue) => this.focusedDiffItem.read(reader) ?? lastValue); public async waitForDiffs(): Promise { for (const d of this.items.get()) { @@ -60,7 +66,13 @@ export class MultiDiffEditorViewModel extends Disposable { } export class DocumentDiffItemViewModel extends Disposable { - public readonly diffEditorViewModel: IDiffEditorViewModel; + /** + * The diff editor view model keeps its inner objects alive. + */ + public readonly diffEditorViewModelRef: RefCounted; + public get diffEditorViewModel(): IDiffEditorViewModel { + return this.diffEditorViewModelRef.object; + } public readonly collapsed = observableValue(this, false); public readonly lastTemplateData = observableValue<{ contentHeight: number; selections: Selection[] | undefined }>( @@ -68,16 +80,39 @@ export class DocumentDiffItemViewModel extends Disposable { { contentHeight: 500, selections: undefined, } ); - public get originalUri(): URI | undefined { return this.entry.value!.original?.uri; } - public get modifiedUri(): URI | undefined { return this.entry.value!.modified?.uri; } + public get originalUri(): URI | undefined { return this.documentDiffItem.original?.uri; } + public get modifiedUri(): URI | undefined { return this.documentDiffItem.modified?.uri; } + + public readonly isActive: IObservable = derived(this, reader => this._editorViewModel.activeDiffItem.read(reader) === this); + + private readonly _isFocusedSource = observableValue>(this, constObservable(false)); + public readonly isFocused = derived(this, reader => this._isFocusedSource.read(reader).read(reader)); + + public setIsFocused(source: IObservable, tx: ITransaction | undefined): void { + this._isFocusedSource.set(source, tx); + } + + private readonly documentDiffItemRef: RefCounted; + public get documentDiffItem(): IDocumentDiffItem { + return this.documentDiffItemRef.object; + } + + public readonly isAlive = observableValue(this, true); constructor( - public readonly entry: LazyPromise, + documentDiffItem: RefCounted, + private readonly _editorViewModel: MultiDiffEditorViewModel, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, ) { super(); + this._register(toDisposable(() => { + this.isAlive.set(false, undefined); + })); + + this.documentDiffItemRef = this._register(documentDiffItem.createNewRef(this)); + function updateOptions(options: IDiffEditorOptions): IDiffEditorOptions { return { ...options, @@ -87,20 +122,26 @@ export class DocumentDiffItemViewModel extends Disposable { }; } - const options = this._instantiationService.createInstance(DiffEditorOptions, updateOptions(this.entry.value!.options || {})); - if (this.entry.value!.onOptionsDidChange) { - this._register(this.entry.value!.onOptionsDidChange(() => { - options.updateOptions(updateOptions(this.entry.value!.options || {})); + const options = this._instantiationService.createInstance(DiffEditorOptions, updateOptions(this.documentDiffItem.options || {})); + if (this.documentDiffItem.onOptionsDidChange) { + this._register(this.documentDiffItem.onOptionsDidChange(() => { + options.updateOptions(updateOptions(this.documentDiffItem.options || {})); })); } - const originalTextModel = this.entry.value!.original ?? this._register(this._modelService.createModel('', null)); - const modifiedTextModel = this.entry.value!.modified ?? this._register(this._modelService.createModel('', null)); + const diffEditorViewModelStore = new DisposableStore(); + const originalTextModel = this.documentDiffItem.original ?? diffEditorViewModelStore.add(this._modelService.createModel('', null)); + const modifiedTextModel = this.documentDiffItem.modified ?? diffEditorViewModelStore.add(this._modelService.createModel('', null)); + diffEditorViewModelStore.add(this.documentDiffItemRef.createNewRef(this)); - this.diffEditorViewModel = this._register(this._instantiationService.createInstance(DiffEditorViewModel, { - original: originalTextModel, - modified: modifiedTextModel, - }, options)); + this.diffEditorViewModelRef = this._register(RefCounted.createWithDisposable( + this._instantiationService.createInstance(DiffEditorViewModel, { + original: originalTextModel, + modified: modifiedTextModel, + }, options), + diffEditorViewModelStore, + this + )); } public getKey(): string { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts index 496b002489b..8275c4f7345 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts @@ -6,8 +6,8 @@ import { Dimension } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { derived, derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable'; -import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; -import { IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; import { IMultiDiffEditorViewState, IMultiDiffResourceId, MultiDiffEditorWidgetImpl } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -81,6 +81,10 @@ export class MultiDiffEditorWidget extends Disposable { public tryGetCodeEditor(resource: URI): { diffEditor: IDiffEditor; editor: ICodeEditor } | undefined { return this._widgetImpl.get().tryGetCodeEditor(resource); } + + public findDocumentDiffItem(resource: URI): IDocumentDiffItem | undefined { + return this._widgetImpl.get().findDocumentDiffItem(resource); + } } export interface RevealOptions { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index bad68c115c1..a8a2f96f7d2 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -5,9 +5,11 @@ import { Dimension, getWindow, h, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { findFirstMaxBy } from 'vs/base/common/arraysFind'; +import { compareBy, numberComparator } from 'vs/base/common/arrays'; +import { findFirstMax } from 'vs/base/common/arraysFind'; +import { BugIndicatingError } from 'vs/base/common/errors'; import { Disposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, autorun, autorunWithStore, derived, derivedObservableWithCache, derivedWithStore, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { IObservable, IReader, autorun, autorunWithStore, derived, derivedWithStore, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ITransaction, disposableObservableValue, globalTransaction, transaction } from 'vs/base/common/observableInternal/base'; import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; @@ -28,10 +30,11 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate'; import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { ObjectPool } from './objectPool'; -import { BugIndicatingError } from 'vs/base/common/errors'; +import { localize } from 'vs/nls'; +import { IDocumentDiffItem } from 'vs/editor/browser/widget/multiDiffEditor/model'; export class MultiDiffEditorWidgetImpl extends Disposable { - private readonly _elements = h('div.monaco-component.multiDiffEditor', [ + private readonly _scrollableElements = h('div.scrollContent', [ h('div@content', { style: { overflow: 'hidden', @@ -41,42 +44,48 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }), ]); - private readonly _sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); - - private readonly _objectPool = this._register(new ObjectPool((data) => { - const template = this._instantiationService.createInstance( - DiffEditorItemTemplate, - this._elements.content, - this._elements.overflowWidgetsDomNode, - this._workbenchUIElementFactory - ); - template.setData(data); - return template; - })); - private readonly _scrollable = this._register(new Scrollable({ forceIntegerValues: false, scheduleAtNextAnimationFrame: (cb) => scheduleAtNextAnimationFrame(getWindow(this._element), cb), smoothScrollDuration: 100, })); - private readonly _scrollableElement = this._register(new SmoothScrollableElement(this._elements.root, { + private readonly _scrollableElement = this._register(new SmoothScrollableElement(this._scrollableElements.root, { vertical: ScrollbarVisibility.Auto, horizontal: ScrollbarVisibility.Auto, useShadows: false, }, this._scrollable)); - public readonly scrollTop = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); - public readonly scrollLeft = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); + private readonly _elements = h('div.monaco-component.multiDiffEditor', {}, [ + h('div', {}, [this._scrollableElement.getDomNode()]), + h('div.placeholder@placeholder', {}, [h('div', [localize('noChangedFiles', 'No Changed Files') as any])]), + ]); - private readonly _viewItems = derivedWithStore(this, + private readonly _sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); + + private readonly _objectPool = this._register(new ObjectPool((data) => { + const template = this._instantiationService.createInstance( + DiffEditorItemTemplate, + this._scrollableElements.content, + this._scrollableElements.overflowWidgetsDomNode, + this._workbenchUIElementFactory + ); + template.setData(data); + return template; + })); + + public readonly scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); + public readonly scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); + + private readonly _viewItemsInfo = derivedWithStore<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this, (reader, store) => { const vm = this._viewModel.read(reader); if (!vm) { - return []; + return { items: [], getItem: _d => { throw new BugIndicatingError(); } }; } - const items = vm.items.read(reader); - return items.map(d => { + const viewModels = vm.items.read(reader); + const map = new Map(); + const items = viewModels.map(d => { const item = store.add(new VirtualizedViewItem(d, this._objectPool, this.scrollLeft, delta => { this._scrollableElement.setScrollPosition({ scrollTop: this._scrollableElement.getScrollPosition().scrollTop + delta }); })); @@ -86,22 +95,29 @@ export class MultiDiffEditorWidgetImpl extends Disposable { item.setViewState(data, tx); }); } + map.set(d, item); return item; }); + return { items, getItem: d => map.get(d)! }; } ); + private readonly _viewItems = this._viewItemsInfo.map(this, items => items.items); + private readonly _spaceBetweenPx = 0; private readonly _totalHeight = this._viewItems.map(this, (items, reader) => items.reduce((r, i) => r + i.contentHeight.read(reader) + this._spaceBetweenPx, 0)); - public readonly activeDiffItem = derived(this, reader => this._viewItems.read(reader).find(i => i.template.read(reader)?.isFocused.read(reader))); - public readonly lastActiveDiffItem = derivedObservableWithCache((reader, lastValue) => this.activeDiffItem.read(reader) ?? lastValue); - public readonly activeControl = derived(this, reader => this.lastActiveDiffItem.read(reader)?.template.read(reader)?.editor); + public readonly activeControl = derived(this, reader => { + const activeDiffItem = this._viewModel.read(reader)?.activeDiffItem.read(reader); + if (!activeDiffItem) { return undefined; } + const viewItem = this._viewItemsInfo.read(reader).getItem(activeDiffItem); + return viewItem.template.read(reader)?.editor; + }); private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._element)); - private readonly _instantiationService = this._parentInstantiationService.createChild( + private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( new ServiceCollection([IContextKeyService, this._contextKeyService]) - ); + )); constructor( private readonly _element: HTMLElement, @@ -135,33 +151,32 @@ export class MultiDiffEditorWidgetImpl extends Disposable { } })); - this._register(autorun((reader) => { - const lastActiveDiffItem = this.lastActiveDiffItem.read(reader); - transaction(tx => { - this._viewModel.read(reader)?.activeDiffItem.set(lastActiveDiffItem?.viewModel, tx); - }); - })); - this._register(autorun((reader) => { /** @description Update widget dimension */ const dimension = this._dimension.read(reader); this._sizeObserver.observe(dimension); })); - this._elements.content.style.position = 'relative'; + this._register(autorun((reader) => { + /** @description Update widget dimension */ + const items = this._viewItems.read(reader); + this._elements.placeholder.classList.toggle('visible', items.length === 0); + })); + + this._scrollableElements.content.style.position = 'relative'; this._register(autorun((reader) => { /** @description Update scroll dimensions */ const height = this._sizeObserver.height.read(reader); - this._elements.root.style.height = `${height}px`; + this._scrollableElements.root.style.height = `${height}px`; const totalHeight = this._totalHeight.read(reader); - this._elements.content.style.height = `${totalHeight}px`; + this._scrollableElements.content.style.height = `${totalHeight}px`; const width = this._sizeObserver.width.read(reader); let scrollWidth = width; const viewItems = this._viewItems.read(reader); - const max = findFirstMaxBy(viewItems, i => i.maxScroll.read(reader).maxScroll); + const max = findFirstMax(viewItems, compareBy(i => i.maxScroll.read(reader).maxScroll, numberComparator)); if (max) { const maxScroll = max.maxScroll.read(reader); scrollWidth = width + maxScroll.maxScroll; @@ -175,7 +190,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }); })); - _element.replaceChildren(this._scrollableElement.getDomNode()); + _element.replaceChildren(this._elements.root); this._register(toDisposable(() => { _element.replaceChildren(); })); @@ -201,13 +216,16 @@ export class MultiDiffEditorWidgetImpl extends Disposable { if (index === -1) { throw new BugIndicatingError('Resource not found in diff editor'); } + const viewItem = viewItems[index]; + this._viewModel.get()!.activeDiffItem.setCache(viewItem.viewModel, undefined); + let scrollTop = 0; for (let i = 0; i < index; i++) { scrollTop += viewItems[i].contentHeight.get() + this._spaceBetweenPx; } this._scrollableElement.setScrollPosition({ scrollTop }); - const diffEditor = viewItems[index].template.get()?.editor; + const diffEditor = viewItem.template.get()?.editor; const editor = 'original' in resource ? diffEditor?.getOriginalEditor() : diffEditor?.getModifiedEditor(); if (editor && options?.range) { editor.revealRangeInCenter(options.range); @@ -246,6 +264,14 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }); } + public findDocumentDiffItem(resource: URI): IDocumentDiffItem | undefined { + const item = this._viewItems.get().find(v => + v.viewModel.diffEditorViewModel.model.modified.uri.toString() === resource.toString() + || v.viewModel.diffEditorViewModel.model.original.uri.toString() === resource.toString() + ); + return item?.viewModel.documentDiffItem; + } + public tryGetCodeEditor(resource: URI): { diffEditor: IDiffEditor; editor: ICodeEditor } | undefined { const item = this._viewItems.get().find(v => v.viewModel.diffEditorViewModel.model.modified.uri.toString() === resource.toString() @@ -255,6 +281,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { if (!editor) { return undefined; } + if (item.viewModel.diffEditorViewModel.model.modified.uri.toString() === resource.toString()) { return { diffEditor: editor, editor: editor.getModifiedEditor() }; } else { @@ -294,7 +321,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { itemContentHeightSumBefore += itemContentHeight + this._spaceBetweenPx; } - this._elements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`; + this._scrollableElements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`; } } @@ -343,6 +370,8 @@ class VirtualizedViewItem extends Disposable { public readonly template = derived(this, reader => this._templateRef.read(reader)?.object); private _isHidden = observableValue(this, false); + private readonly _isFocused = derived(this, reader => this.template.read(reader)?.isFocused.read(reader) ?? false); + constructor( public readonly viewModel: DocumentDiffItemViewModel, private readonly _objectPool: ObjectPool, @@ -351,6 +380,8 @@ class VirtualizedViewItem extends Disposable { ) { super(); + this.viewModel.setIsFocused(this._isFocused, undefined); + this._register(autorun((reader) => { const scrollLeft = this._scrollLeft.read(reader); this._templateRef.read(reader)?.object.setScrollLeft(scrollLeft); @@ -375,7 +406,7 @@ class VirtualizedViewItem extends Disposable { } public override toString(): string { - return `VirtualViewItem(${this.viewModel.entry.value!.modified?.uri.toString()})`; + return `VirtualViewItem(${this.viewModel.documentDiffItem.modified?.uri.toString()})`; } public getKey(): string { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/style.css b/src/vs/editor/browser/widget/multiDiffEditor/style.css index c540a46b3f1..fc9c877bf78 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/style.css +++ b/src/vs/editor/browser/widget/multiDiffEditor/style.css @@ -5,9 +5,36 @@ .monaco-component.multiDiffEditor { background: var(--vscode-multiDiffEditor-background); + + position: relative; + + height: 100%; + width: 100%; + overflow-y: hidden; - .focused { + > div { + position: absolute; + top: 0px; + left: 0px; + + height: 100%; + width: 100%; + + &.placeholder { + visibility: hidden; + + &.visible { + visibility: visible; + } + + display: grid; + place-items: center; + place-content: center; + } + } + + .active { --vscode-multiDiffEditor-border: var(--vscode-focusBorder); } @@ -36,14 +63,10 @@ } .header-content { - margin: 8px 8px 0px 8px; + margin: 8px 0px 0px 0px; padding: 4px 5px; border-top: 1px solid var(--vscode-multiDiffEditor-border); - border-right: 1px solid var(--vscode-multiDiffEditor-border); - border-left: 1px solid var(--vscode-multiDiffEditor-border); - border-top-left-radius: 2px; - border-top-right-radius: 2px; display: flex; align-items: center; @@ -107,13 +130,7 @@ display: flex; flex-direction: column; - margin-right: 8px; - margin-left: 8px; - - border-right: 1px solid var(--vscode-multiDiffEditor-border); - border-left: 1px solid var(--vscode-multiDiffEditor-border); border-bottom: 1px solid var(--vscode-multiDiffEditor-border); - border-radius: 2px; overflow: hidden; } diff --git a/src/vs/editor/common/config/diffEditor.ts b/src/vs/editor/common/config/diffEditor.ts index 2d0a312357e..ff97a530fb6 100644 --- a/src/vs/editor/common/config/diffEditor.ts +++ b/src/vs/editor/common/config/diffEditor.ts @@ -24,6 +24,7 @@ export const diffEditorDefaultOptions = { experimental: { showMoves: false, showEmptyDecorations: true, + useTrueInlineView: false, }, hideUnchangedRegions: { enabled: false, @@ -35,4 +36,5 @@ export const diffEditorDefaultOptions = { onlyShowAccessibleDiffViewer: false, renderSideBySideInlineBreakpoint: 900, useInlineViewWhenSpaceIsLimited: true, + compactMode: false, } satisfies ValidDiffEditorBaseOptions; diff --git a/src/vs/editor/common/config/editorConfiguration.ts b/src/vs/editor/common/config/editorConfiguration.ts index 415f8532d85..ccaa98425d7 100644 --- a/src/vs/editor/common/config/editorConfiguration.ts +++ b/src/vs/editor/common/config/editorConfiguration.ts @@ -7,12 +7,17 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ConfigurationChangedEvent, IComputedEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; +import { MenuId } from 'vs/platform/actions/common/actions'; export interface IEditorConfiguration extends IDisposable { /** * Is this a simple widget (not a real code editor)? */ readonly isSimpleWidget: boolean; + /** + * The context menu id for the editor. + */ + readonly contextMenuId: MenuId; /** * Computed editor options. */ diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index ab2b8cc70c6..c2d124936b4 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -109,6 +109,12 @@ const editorConfiguration: IConfigurationNode = { description: nls.localize('editor.experimental.asyncTokenizationVerification', "Controls whether async tokenization should be verified against legacy background tokenization. Might slow down tokenization. For debugging only."), tags: ['experimental'], }, + 'editor.experimental.treeSitterTelemetry': { + type: 'boolean', + default: false, + markdownDescription: nls.localize('editor.experimental.treeSitterTelemetry', "Controls whether tree sitter parsing should be turned on and telemetry collected. Setting `editor.experimental.preferTreeSitter` for specific languages will take precedence."), + tags: ['experimental'] + }, 'editor.language.brackets': { type: ['array', 'null'], default: null, // We want to distinguish the empty array from not configured. @@ -247,7 +253,12 @@ const editorConfiguration: IConfigurationNode = { type: 'boolean', default: diffEditorDefaultOptions.experimental.showEmptyDecorations, description: nls.localize('showEmptyDecorations', "Controls whether the diff editor shows empty decorations to see where characters got inserted or deleted."), - } + }, + 'diffEditor.experimental.useTrueInlineView': { + type: 'boolean', + default: diffEditorDefaultOptions.experimental.useTrueInlineView, + description: nls.localize('useTrueInlineView', "If enabled and the editor uses the inline view, word changes are rendered inline."), + }, } }; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index b2a486bce5c..10e4d8236d3 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -692,6 +692,13 @@ export interface IEditorOptions { * Defaults to false. */ peekWidgetDefaultFocus?: 'tree' | 'editor'; + + /** + * Sets a placeholder for the editor. + * If set, the placeholder is shown if the editor is empty. + */ + placeholder?: string | undefined; + /** * Controls whether the definition link opens element in the peek widget. * Defaults to false. @@ -764,75 +771,96 @@ export interface IDiffEditorBaseOptions { * Defaults to true. */ enableSplitViewResizing?: boolean; + /** * The default ratio when rendering side-by-side editors. * Must be a number between 0 and 1, min sizes apply. * Defaults to 0.5 */ splitViewDefaultRatio?: number; + /** * Render the differences in two side-by-side editors. * Defaults to true. */ renderSideBySide?: boolean; + /** * When `renderSideBySide` is enabled, `useInlineViewWhenSpaceIsLimited` is set, * and the diff editor has a width less than `renderSideBySideInlineBreakpoint`, the inline view is used. */ renderSideBySideInlineBreakpoint?: number | undefined; + /** * When `renderSideBySide` is enabled, `useInlineViewWhenSpaceIsLimited` is set, * and the diff editor has a width less than `renderSideBySideInlineBreakpoint`, the inline view is used. */ useInlineViewWhenSpaceIsLimited?: boolean; + + /** + * If set, the diff editor is optimized for small views. + * Defaults to `false`. + */ + compactMode?: boolean; + /** * Timeout in milliseconds after which diff computation is cancelled. * Defaults to 5000. */ maxComputationTime?: number; + /** * Maximum supported file size in MB. * Defaults to 50. */ maxFileSize?: number; + /** * Compute the diff by ignoring leading/trailing whitespace * Defaults to true. */ ignoreTrimWhitespace?: boolean; + /** * Render +/- indicators for added/deleted changes. * Defaults to true. */ renderIndicators?: boolean; + /** * Shows icons in the glyph margin to revert changes. * Default to true. */ renderMarginRevertIcon?: boolean; + /** * Indicates if the gutter menu should be rendered. */ renderGutterMenu?: boolean; + /** * Original model should be editable? * Defaults to false. */ originalEditable?: boolean; + /** * Should the diff editor enable code lens? * Defaults to false. */ diffCodeLens?: boolean; + /** * Is the diff editor should render overview ruler * Defaults to true */ renderOverviewRuler?: boolean; + /** * Control the wrapping of the diff editor. */ diffWordWrap?: 'off' | 'on' | 'inherit'; + /** * Diff Algorithm */ @@ -850,6 +878,11 @@ export interface IDiffEditorBaseOptions { showMoves?: boolean; showEmptyDecorations?: boolean; + + /** + * Only applies when `renderSideBySide` is set to false. + */ + useTrueInlineView?: boolean; }; /** @@ -1909,12 +1942,14 @@ export interface IGotoLocationOptions { multipleDeclarations?: GoToLocationValues; multipleImplementations?: GoToLocationValues; multipleReferences?: GoToLocationValues; + multipleTests?: GoToLocationValues; alternativeDefinitionCommand?: string; alternativeTypeDefinitionCommand?: string; alternativeDeclarationCommand?: string; alternativeImplementationCommand?: string; alternativeReferenceCommand?: string; + alternativeTestsCommand?: string; } /** @@ -1932,11 +1967,13 @@ class EditorGoToLocation extends BaseEditorOption(input.multipleDeclarations, 'peek', ['peek', 'gotoAndPeek', 'goto']), multipleImplementations: input.multipleImplementations ?? stringSet(input.multipleImplementations, 'peek', ['peek', 'gotoAndPeek', 'goto']), multipleReferences: input.multipleReferences ?? stringSet(input.multipleReferences, 'peek', ['peek', 'gotoAndPeek', 'goto']), + multipleTests: input.multipleTests ?? stringSet(input.multipleTests, 'peek', ['peek', 'gotoAndPeek', 'goto']), alternativeDefinitionCommand: EditorStringOption.string(input.alternativeDefinitionCommand, this.defaultValue.alternativeDefinitionCommand), alternativeTypeDefinitionCommand: EditorStringOption.string(input.alternativeTypeDefinitionCommand, this.defaultValue.alternativeTypeDefinitionCommand), alternativeDeclarationCommand: EditorStringOption.string(input.alternativeDeclarationCommand, this.defaultValue.alternativeDeclarationCommand), alternativeImplementationCommand: EditorStringOption.string(input.alternativeImplementationCommand, this.defaultValue.alternativeImplementationCommand), alternativeReferenceCommand: EditorStringOption.string(input.alternativeReferenceCommand, this.defaultValue.alternativeReferenceCommand), + alternativeTestsCommand: EditorStringOption.string(input.alternativeTestsCommand, this.defaultValue.alternativeTestsCommand), }; } } @@ -2760,7 +2799,7 @@ export type EditorLightbulbOptions = Readonly> class EditorLightbulb extends BaseEditorOption { constructor() { - const defaults: EditorLightbulbOptions = { enabled: ShowLightbulbIconMode.OnCode }; + const defaults: EditorLightbulbOptions = { enabled: ShowLightbulbIconMode.On }; super( EditorOption.lightbulb, 'lightbulb', defaults, { @@ -3071,6 +3110,10 @@ export interface IEditorMinimapOptions { * Font size of section headers. Defaults to 9. */ sectionHeaderFontSize?: number; + /** + * Spacing between the section header characters (in CSS px). Defaults to 1. + */ + sectionHeaderLetterSpacing?: number; } /** @@ -3093,6 +3136,7 @@ class EditorMinimap extends BaseEditorOption { + constructor() { + super(EditorOption.placeholder, 'placeholder', undefined); + } + + public validate(input: any): string | undefined { + if (typeof input === 'undefined') { + return this.defaultValue; + } + if (typeof input === 'string') { + return input; + } + return this.defaultValue; + } +} +//#endregion + //#region quickSuggestions export type QuickSuggestionsValue = 'on' | 'inline' | 'off'; @@ -3394,7 +3463,7 @@ class EditorQuickSuggestions extends BaseEditorOption checkAdjacentItems(edits, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition()))); } @@ -161,10 +166,19 @@ export class SingleTextEdit { static equals(first: SingleTextEdit, second: SingleTextEdit) { return first.range.equalsRange(second.range) && first.text === second.text; } + + public toSingleEditOperation(): ISingleEditOperation { + return { + range: this.range, + text: this.text, + }; + } } function rangeFromPositions(start: Position, end: Position): Range { - if (!start.isBeforeOrEqual(end)) { + if (start.lineNumber === end.lineNumber && start.column === Number.MAX_SAFE_INTEGER) { + return Range.fromPositions(end, end); + } else if (!start.isBeforeOrEqual(end)) { throw new BugIndicatingError('start must be before end'); } return new Range(start.lineNumber, start.column, end.lineNumber, end.column); @@ -211,6 +225,15 @@ export class LineBasedText extends AbstractText { } } +export class ArrayText extends LineBasedText { + constructor(lines: string[]) { + super( + lineNumber => lines[lineNumber - 1], + lines.length + ); + } +} + export class StringText extends AbstractText { private readonly _t = new PositionOffsetTransformer(this.value); diff --git a/src/vs/editor/common/core/textLength.ts b/src/vs/editor/common/core/textLength.ts index 632895c55fd..bee3897d5fd 100644 --- a/src/vs/editor/common/core/textLength.ts +++ b/src/vs/editor/common/core/textLength.ts @@ -71,6 +71,13 @@ export class TextLength { return this.columnCount > other.columnCount; } + public isGreaterThanOrEqualTo(other: TextLength): boolean { + if (this.lineCount !== other.lineCount) { + return this.lineCount > other.lineCount; + } + return this.columnCount >= other.columnCount; + } + public equals(other: TextLength): boolean { return this.lineCount === other.lineCount && this.columnCount === other.columnCount; } diff --git a/src/vs/editor/common/cursor/cursor.ts b/src/vs/editor/common/cursor/cursor.ts index 4df16ec8db2..f84b5135d13 100644 --- a/src/vs/editor/common/cursor/cursor.ts +++ b/src/vs/editor/common/cursor/cursor.ts @@ -10,7 +10,8 @@ import { CursorConfiguration, CursorState, EditOperationResult, EditOperationTyp import { CursorContext } from 'vs/editor/common/cursor/cursorContext'; import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; -import { CompositionOutcome, TypeOperations, TypeWithAutoClosingCommand } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { CompositionOutcome, TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { BaseTypeWithAutoClosingCommand } from 'vs/editor/common/cursor/cursorTypeEditOperations'; import { Position } from 'vs/editor/common/core/position'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection'; @@ -367,7 +368,7 @@ export class CursorsController extends Disposable { for (let i = 0; i < opResult.commands.length; i++) { const command = opResult.commands[i]; - if (command instanceof TypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) { + if (command instanceof BaseTypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) { autoClosedCharactersRanges.push(command.closeCharacterRange); autoClosedEnclosingRanges.push(command.enclosingRange); } diff --git a/src/vs/editor/common/cursor/cursorCollection.ts b/src/vs/editor/common/cursor/cursorCollection.ts index f2e8a8b4388..626c139cbb6 100644 --- a/src/vs/editor/common/cursor/cursorCollection.ts +++ b/src/vs/editor/common/cursor/cursorCollection.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareBy } from 'vs/base/common/arrays'; -import { findLastMaxBy, findFirstMinBy } from 'vs/base/common/arraysFind'; +import { findLastMax, findFirstMin } from 'vs/base/common/arraysFind'; import { CursorState, PartialCursorState } from 'vs/editor/common/cursorCommon'; import { CursorContext } from 'vs/editor/common/cursor/cursorContext'; import { Cursor } from 'vs/editor/common/cursor/oneCursor'; @@ -73,14 +73,14 @@ export class CursorCollection { } public getTopMostViewPosition(): Position { - return findFirstMinBy( + return findFirstMin( this.cursors, compareBy(c => c.viewState.position, Position.compare) )!.viewState.position; } public getBottomMostViewPosition(): Position { - return findLastMaxBy( + return findLastMax( this.cursors, compareBy(c => c.viewState.position, Position.compare) )!.viewState.position; diff --git a/src/vs/editor/common/cursor/cursorMoveCommands.ts b/src/vs/editor/common/cursor/cursorMoveCommands.ts index 8ae3d67b0fd..78c553732a9 100644 --- a/src/vs/editor/common/cursor/cursorMoveCommands.ts +++ b/src/vs/editor/common/cursor/cursorMoveCommands.ts @@ -596,7 +596,7 @@ export namespace CursorMove { return true; }; - export const metadata = { + export const metadata: ICommandMetadata = { description: 'Move cursor to a logical position in the view', args: [ { diff --git a/src/vs/editor/common/cursor/cursorTypeEditOperations.ts b/src/vs/editor/common/cursor/cursorTypeEditOperations.ts new file mode 100644 index 00000000000..df17d2f3918 --- /dev/null +++ b/src/vs/editor/common/cursor/cursorTypeEditOperations.ts @@ -0,0 +1,1030 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CharCode } from 'vs/base/common/charCode'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import * as strings from 'vs/base/common/strings'; +import { ReplaceCommand, ReplaceCommandWithOffsetCursorState, ReplaceCommandWithoutChangingPosition, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; +import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; +import { SurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; +import { CursorConfiguration, EditOperationResult, EditOperationType, ICursorSimpleModel, isQuote } from 'vs/editor/common/cursorCommon'; +import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Position } from 'vs/editor/common/core/position'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; +import { getIndentationAtPosition } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; +import { EditorAutoClosingStrategy, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; +import { getIndentActionForType, getIndentForEnter, getInheritIndentForLine } from 'vs/editor/common/languages/autoIndent'; +import { getEnterAction } from 'vs/editor/common/languages/enterAction'; + +export class AutoIndentOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) { + const indentationForSelections: { selection: Selection; indentation: string }[] = []; + for (const selection of selections) { + const indentation = this._findActualIndentationForSelection(config, model, selection, ch); + if (indentation === null) { + // Auto indentation failed + return; + } + indentationForSelections.push({ selection, indentation }); + } + const autoClosingPairClose = AutoClosingOpenCharTypeOperation.getAutoClosingPairClose(config, model, selections, ch, false); + return this._getIndentationAndAutoClosingPairEdits(config, model, indentationForSelections, ch, autoClosingPairClose); + } + return; + } + + private static _isAutoIndentType(config: CursorConfiguration, model: ITextModel, selections: Selection[]): boolean { + if (config.autoIndent < EditorAutoIndentStrategy.Full) { + return false; + } + for (let i = 0, len = selections.length; i < len; i++) { + if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { + return false; + } + } + return true; + } + + private static _findActualIndentationForSelection(config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): string | null { + const actualIndentation = getIndentActionForType(config, model, selection, ch, { + shiftIndent: (indentation) => { + return shiftIndent(config, indentation); + }, + unshiftIndent: (indentation) => { + return unshiftIndent(config, indentation); + }, + }, config.languageConfigurationService); + + if (actualIndentation === null) { + return null; + } + + const currentIndentation = getIndentationAtPosition(model, selection.startLineNumber, selection.startColumn); + if (actualIndentation === config.normalizeIndentation(currentIndentation)) { + return null; + } + return actualIndentation; + } + + private static _getIndentationAndAutoClosingPairEdits(config: CursorConfiguration, model: ITextModel, indentationForSelections: { selection: Selection; indentation: string }[], ch: string, autoClosingPairClose: string | null): EditOperationResult { + const commands: ICommand[] = indentationForSelections.map(({ selection, indentation }) => { + if (autoClosingPairClose !== null) { + // Apply both auto closing pair edits and auto indentation edits + const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, false); + return new TypeWithIndentationAndAutoClosingCommand(indentationEdit, selection, ch, autoClosingPairClose); + } else { + // Apply only auto indentation edits + const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, true); + return typeCommand(indentationEdit.range, indentationEdit.text, false); + } + }); + const editOptions = { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false }; + return new EditOperationResult(EditOperationType.TypingOther, commands, editOptions); + } + + private static _getEditFromIndentationAndSelection(config: CursorConfiguration, model: ITextModel, indentation: string, selection: Selection, ch: string, includeChInEdit: boolean = true): { range: Range; text: string } { + const startLineNumber = selection.startLineNumber; + const firstNonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(startLineNumber); + let text: string = config.normalizeIndentation(indentation); + if (firstNonWhitespaceColumn !== 0) { + const startLine = model.getLineContent(startLineNumber); + text += startLine.substring(firstNonWhitespaceColumn - 1, selection.startColumn - 1); + } + text += includeChInEdit ? ch : ''; + const range = new Range(startLineNumber, 1, selection.endLineNumber, selection.endColumn); + return { range, text }; + } +} + +export class AutoClosingOvertypeOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult | undefined { + if (isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { + return this._runAutoClosingOvertype(prevEditOperationType, selections, ch); + } + return; + } + + private static _runAutoClosingOvertype(prevEditOperationType: EditOperationType, selections: Selection[], ch: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const position = selection.getPosition(); + const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); + commands[i] = new ReplaceCommand(typeSelection, ch); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), + shouldPushStackElementAfter: false + }); + } +} + +export class AutoClosingOvertypeWithInterceptorsOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult | undefined { + if (isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { + // Unfortunately, the close character is at this point "doubled", so we need to delete it... + const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } + return; + } +} + +export class AutoClosingOpenCharTypeOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition) { + const autoClosingPairClose = this.getAutoClosingPairClose(config, model, selections, ch, chIsAlreadyTyped); + if (autoClosingPairClose !== null) { + return this._runAutoClosingOpenCharType(selections, ch, chIsAlreadyTyped, autoClosingPairClose); + } + } + return; + } + + private static _runAutoClosingOpenCharType(selections: Selection[], ch: string, chIsAlreadyTyped: boolean, autoClosingPairClose: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + commands[i] = new TypeWithAutoClosingCommand(selection, ch, !chIsAlreadyTyped, autoClosingPairClose); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } + + public static getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null { + for (const selection of selections) { + if (!selection.isEmpty()) { + return null; + } + } + // This method is called both when typing (regularly) and when composition ends + // This means that we need to work with a text buffer where sometimes `ch` is not + // there (it is being typed right now) or with a text buffer where `ch` has already been typed + // + // In order to avoid adding checks for `chIsAlreadyTyped` in all places, we will work + // with two conceptual positions, the position before `ch` and the position after `ch` + // + const positions: { lineNumber: number; beforeColumn: number; afterColumn: number }[] = selections.map((s) => { + const position = s.getPosition(); + if (chIsAlreadyTyped) { + return { lineNumber: position.lineNumber, beforeColumn: position.column - ch.length, afterColumn: position.column }; + } else { + return { lineNumber: position.lineNumber, beforeColumn: position.column, afterColumn: position.column }; + } + }); + // Find the longest auto-closing open pair in case of multiple ending in `ch` + // e.g. when having [f","] and [","], it picks [f","] if the character before is f + const pair = this._findAutoClosingPairOpen(config, model, positions.map(p => new Position(p.lineNumber, p.beforeColumn)), ch); + if (!pair) { + return null; + } + let autoCloseConfig: EditorAutoClosingStrategy; + let shouldAutoCloseBefore: (ch: string) => boolean; + + const chIsQuote = isQuote(ch); + if (chIsQuote) { + autoCloseConfig = config.autoClosingQuotes; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.quote; + } else { + const pairIsForComments = config.blockCommentStartToken ? pair.open.includes(config.blockCommentStartToken) : false; + if (pairIsForComments) { + autoCloseConfig = config.autoClosingComments; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.comment; + } else { + autoCloseConfig = config.autoClosingBrackets; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket; + } + } + if (autoCloseConfig === 'never') { + return null; + } + // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship + // e.g. when having [(,)] and [(*,*)] + // - when typing (, the resulting state is (|) + // - when typing *, the desired resulting state is (*|*), not (*|*)) + const containedPair = this._findContainedAutoClosingPair(config, pair); + const containedPairClose = containedPair ? containedPair.close : ''; + let isContainedPairPresent = true; + + for (const position of positions) { + const { lineNumber, beforeColumn, afterColumn } = position; + const lineText = model.getLineContent(lineNumber); + const lineBefore = lineText.substring(0, beforeColumn - 1); + const lineAfter = lineText.substring(afterColumn - 1); + + if (!lineAfter.startsWith(containedPairClose)) { + isContainedPairPresent = false; + } + // Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows + if (lineAfter.length > 0) { + const characterAfter = lineAfter.charAt(0); + const isBeforeCloseBrace = this._isBeforeClosingBrace(config, lineAfter); + if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { + return null; + } + } + // Do not auto-close ' or " after a word character + if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { + const wordSeparators = getMapForWordSeparators(config.wordSeparators, []); + if (lineBefore.length > 0) { + const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1); + if (wordSeparators.get(characterBefore) === WordCharacterClass.Regular) { + return null; + } + } + } + if (!model.tokenization.isCheapToTokenize(lineNumber)) { + // Do not force tokenization + return null; + } + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); + if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { + return null; + } + // Typing for example a quote could either start a new string, in which case auto-closing is desirable + // or it could end a previously started string, in which case auto-closing is not desirable + // + // In certain cases, it is really not possible to look at the previous token to determine + // what would happen. That's why we do something really unusual, we pretend to type a different + // character and ask the tokenizer what the outcome of doing that is: after typing a neutral + // character, are we in a string (i.e. the quote would most likely end a string) or not? + // + const neutralCharacter = pair.findNeutralCharacter(); + if (neutralCharacter) { + const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); + if (!pair.isOK(tokenType)) { + return null; + } + } + } + if (isContainedPairPresent) { + return pair.close.substring(0, pair.close.length - containedPairClose.length); + } else { + return pair.close; + } + } + + /** + * Find another auto-closing pair that is contained by the one passed in. + * + * e.g. when having [(,)] and [(*,*)] as auto-closing pairs + * this method will find [(,)] as a containment pair for [(*,*)] + */ + private static _findContainedAutoClosingPair(config: CursorConfiguration, pair: StandardAutoClosingPairConditional): StandardAutoClosingPairConditional | null { + if (pair.open.length <= 1) { + return null; + } + const lastChar = pair.close.charAt(pair.close.length - 1); + // get candidates with the same last character as close + const candidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || []; + let result: StandardAutoClosingPairConditional | null = null; + for (const candidate of candidates) { + if (candidate.open !== pair.open && pair.open.includes(candidate.open) && pair.close.endsWith(candidate.close)) { + if (!result || candidate.open.length > result.open.length) { + result = candidate; + } + } + } + return result; + } + + /** + * Determine if typing `ch` at all `positions` in the `model` results in an + * auto closing open sequence being typed. + * + * Auto closing open sequences can consist of multiple characters, which + * can lead to ambiguities. In such a case, the longest auto-closing open + * sequence is returned. + */ + private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null { + const candidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); + if (!candidates) { + return null; + } + // Determine which auto-closing pair it is + let result: StandardAutoClosingPairConditional | null = null; + for (const candidate of candidates) { + if (result === null || candidate.open.length > result.open.length) { + let candidateIsMatch = true; + for (const position of positions) { + const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - candidate.open.length + 1, position.lineNumber, position.column)); + if (relevantText + ch !== candidate.open) { + candidateIsMatch = false; + break; + } + } + if (candidateIsMatch) { + result = candidate; + } + } + } + return result; + } + + private static _isBeforeClosingBrace(config: CursorConfiguration, lineAfter: string) { + // If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false + const nextChar = lineAfter.charAt(0); + const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || []; + const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || []; + + const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open)); + const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close)); + + return !isBeforeStartingBrace && isBeforeClosingBrace; + } +} + +export class SurroundSelectionOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && this._isSurroundSelectionType(config, model, selections, ch)) { + return this._runSurroundSelectionType(config, selections, ch); + } + return; + } + + private static _runSurroundSelectionType(config: CursorConfiguration, selections: Selection[], ch: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const closeCharacter = config.surroundingPairs[ch]; + commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter); + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } + + private static _isSurroundSelectionType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { + if (!shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { + return false; + } + const isTypingAQuoteCharacter = isQuote(ch); + for (const selection of selections) { + if (selection.isEmpty()) { + return false; + } + let selectionContainsOnlyWhitespace = true; + for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { + const lineText = model.getLineContent(lineNumber); + const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); + const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); + const selectedText = lineText.substring(startIndex, endIndex); + if (/[^ \t]/.test(selectedText)) { + // this selected text contains something other than whitespace + selectionContainsOnlyWhitespace = false; + break; + } + } + if (selectionContainsOnlyWhitespace) { + return false; + } + if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) { + const selectionText = model.getValueInRange(selection); + if (isQuote(selectionText)) { + // Typing a quote character on top of another quote character + // => disable surround selection type + return false; + } + } + } + return true; + } +} + +export class InterceptorElectricCharOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + // Electric characters make sense only when dealing with a single cursor, + // as multiple cursors typing brackets for example would interfer with bracket matching + if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) { + const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch); + if (r) { + return r; + } + } + return; + } + + private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { + if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { + return true; + } + return false; + } + + private static _typeInterceptorElectricChar(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): EditOperationResult | null { + if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) { + return null; + } + const position = selection.getPosition(); + model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); + let electricAction: IElectricAction | null; + try { + electricAction = config.onElectricCharacter(ch, lineTokens, position.column); + } catch (e) { + onUnexpectedError(e); + return null; + } + if (!electricAction) { + return null; + } + if (electricAction.matchOpenBracket) { + const endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; + const match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { + lineNumber: position.lineNumber, + column: endColumn + }, 500 /* give at most 500ms to compute */); + if (match) { + if (match.startLineNumber === position.lineNumber) { + // matched something on the same line => no change in indentation + return null; + } + const matchLine = model.getLineContent(match.startLineNumber); + const matchLineIndentation = strings.getLeadingWhitespace(matchLine); + const newIndentation = config.normalizeIndentation(matchLineIndentation); + const lineText = model.getLineContent(position.lineNumber); + const lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column; + const prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1); + const typeText = newIndentation + prefix + ch; + const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); + const command = new ReplaceCommand(typeSelection, typeText); + return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { + shouldPushStackElementBefore: false, + shouldPushStackElementAfter: true + }); + } + } + return null; + } +} + +export class SimpleCharacterTypeOperation { + + public static getEdits(prevEditOperationType: EditOperationType, selections: Selection[], ch: string): EditOperationResult { + // A simple character type + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = new ReplaceCommand(selections[i], ch); + } + + const opType = getTypingOperation(ch, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), + shouldPushStackElementAfter: false + }); + } +} + +export class EnterOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && ch === '\n') { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = this._enter(config, model, false, selections[i]); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false, + }); + } + return; + } + + private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { + if (config.autoIndent === EditorAutoIndentStrategy.None) { + return typeCommand(range, '\n', keepPosition); + } + if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { + const lineText = model.getLineContent(range.startLineNumber); + const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); + return typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); + } + const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService); + if (r) { + if (r.indentAction === IndentAction.None) { + // Nothing special + return typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); + + } else if (r.indentAction === IndentAction.Indent) { + // Indent once + return typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); + + } else if (r.indentAction === IndentAction.IndentOutdent) { + // Ultra special + const normalIndent = config.normalizeIndentation(r.indentation); + const increasedIndent = config.normalizeIndentation(r.indentation + r.appendText); + const typeText = '\n' + increasedIndent + '\n' + normalIndent; + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, typeText, true); + } else { + return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); + } + } else if (r.indentAction === IndentAction.Outdent) { + const actualIndentation = unshiftIndent(config, r.indentation); + return typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); + } + } + + const lineText = model.getLineContent(range.startLineNumber); + const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); + + if (config.autoIndent >= EditorAutoIndentStrategy.Full) { + const ir = getIndentForEnter(config.autoIndent, model, range, { + unshiftIndent: (indent) => { + return unshiftIndent(config, indent); + }, + shiftIndent: (indent) => { + return shiftIndent(config, indent); + }, + normalizeIndentation: (indent) => { + return config.normalizeIndentation(indent); + } + }, config.languageConfigurationService); + + if (ir) { + let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition()); + const oldEndColumn = range.endColumn; + const newLineContent = model.getLineContent(range.endLineNumber); + const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); + if (firstNonWhitespace >= 0) { + range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1)); + } else { + range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber)); + } + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); + } else { + let offset = 0; + if (oldEndColumn <= firstNonWhitespace + 1) { + if (!config.insertSpaces) { + oldEndViewColumn = Math.ceil(oldEndViewColumn / config.indentSize); + } + offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); + } + return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); + } + } + } + return typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); + } + + + public static lineInsertBefore(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { + if (model === null || selections === null) { + return []; + } + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + let lineNumber = selections[i].positionLineNumber; + if (lineNumber === 1) { + commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n'); + } else { + lineNumber--; + const column = model.getLineMaxColumn(lineNumber); + + commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); + } + } + return commands; + } + + public static lineInsertAfter(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { + if (model === null || selections === null) { + return []; + } + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const lineNumber = selections[i].positionLineNumber; + const column = model.getLineMaxColumn(lineNumber); + commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); + } + return commands; + } + + public static lineBreakInsert(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = this._enter(config, model, true, selections[i]); + } + return commands; + } +} + +export class PasteOperation { + + public static getEdits(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]) { + const distributedPaste = this._distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText); + if (distributedPaste) { + selections = selections.sort(Range.compareRangesUsingStarts); + return this._distributedPaste(config, model, selections, distributedPaste); + } else { + return this._simplePaste(config, model, selections, text, pasteOnNewLine); + } + } + + private static _distributePasteToCursors(config: CursorConfiguration, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): string[] | null { + if (pasteOnNewLine) { + return null; + } + if (selections.length === 1) { + return null; + } + if (multicursorText && multicursorText.length === selections.length) { + return multicursorText; + } + if (config.multiCursorPaste === 'spread') { + // Try to spread the pasted text in case the line count matches the cursor count + // Remove trailing \n if present + if (text.charCodeAt(text.length - 1) === CharCode.LineFeed) { + text = text.substring(0, text.length - 1); + } + // Remove trailing \r if present + if (text.charCodeAt(text.length - 1) === CharCode.CarriageReturn) { + text = text.substring(0, text.length - 1); + } + const lines = strings.splitLines(text); + if (lines.length === selections.length) { + return lines; + } + } + return null; + } + + private static _distributedPaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string[]): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = new ReplaceCommand(selections[i], text[i]); + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } + + private static _simplePaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const position = selection.getPosition(); + if (pasteOnNewLine && !selection.isEmpty()) { + pasteOnNewLine = false; + } + if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) { + pasteOnNewLine = false; + } + if (pasteOnNewLine) { + // Paste entire line at the beginning of line + const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1); + commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection, true); + } else { + commands[i] = new ReplaceCommand(selection, text); + } + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } +} + +export class CompositionOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number) { + const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), + shouldPushStackElementAfter: false + }); + } + + private static _compositionType(model: ITextModel, selection: Selection, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): ICommand | null { + if (!selection.isEmpty()) { + // looks like https://github.com/microsoft/vscode/issues/2773 + // where a cursor operation occurred before a canceled composition + // => ignore composition + return null; + } + const pos = selection.getPosition(); + const startColumn = Math.max(1, pos.column - replacePrevCharCnt); + const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt); + const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn); + const oldText = model.getValueInRange(range); + if (oldText === text && positionDelta === 0) { + // => ignore composition that doesn't do anything + return null; + } + return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta); + } +} + +export class TypeWithoutInterceptorsOperation { + + public static getEdits(prevEditOperationType: EditOperationType, selections: Selection[], str: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = new ReplaceCommand(selections[i], str); + } + const opType = getTypingOperation(str, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), + shouldPushStackElementAfter: false + }); + } +} + +export class TabOperation { + + public static getCommands(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + if (selection.isEmpty()) { + const lineText = model.getLineContent(selection.startLineNumber); + if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { + let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); + goodIndent = goodIndent || '\t'; + const possibleTypeText = config.normalizeIndentation(goodIndent); + if (!lineText.startsWith(possibleTypeText)) { + commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true); + continue; + } + } + commands[i] = this._replaceJumpToNextIndent(config, model, selection, true); + } else { + if (selection.startLineNumber === selection.endLineNumber) { + const lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber); + if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) { + // This is a single line selection that is not the entire line + commands[i] = this._replaceJumpToNextIndent(config, model, selection, false); + continue; + } + } + commands[i] = new ShiftCommand(selection, { + isUnshift: false, + tabSize: config.tabSize, + indentSize: config.indentSize, + insertSpaces: config.insertSpaces, + useTabStops: config.useTabStops, + autoIndent: config.autoIndent + }, config.languageConfigurationService); + } + } + return commands; + } + + private static _goodIndentForLine(config: CursorConfiguration, model: ITextModel, lineNumber: number): string | null { + let action: IndentAction | EnterAction | null = null; + let indentation: string = ''; + const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService); + if (expectedIndentAction) { + action = expectedIndentAction.action; + indentation = expectedIndentAction.indentation; + } else if (lineNumber > 1) { + let lastLineNumber: number; + for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { + const lineText = model.getLineContent(lastLineNumber); + const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText); + if (nonWhitespaceIdx >= 0) { + break; + } + } + if (lastLineNumber < 1) { + // No previous line with content found + return null; + } + const maxColumn = model.getLineMaxColumn(lastLineNumber); + const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService); + if (expectedEnterAction) { + indentation = expectedEnterAction.indentation + expectedEnterAction.appendText; + } + } + if (action) { + if (action === IndentAction.Indent) { + indentation = shiftIndent(config, indentation); + } + if (action === IndentAction.Outdent) { + indentation = unshiftIndent(config, indentation); + } + indentation = config.normalizeIndentation(indentation); + } + if (!indentation) { + return null; + } + return indentation; + } + + private static _replaceJumpToNextIndent(config: CursorConfiguration, model: ICursorSimpleModel, selection: Selection, insertsAutoWhitespace: boolean): ReplaceCommand { + let typeText = ''; + const position = selection.getStartPosition(); + if (config.insertSpaces) { + const visibleColumnFromColumn = config.visibleColumnFromColumn(model, position); + const indentSize = config.indentSize; + const spacesCnt = indentSize - (visibleColumnFromColumn % indentSize); + for (let i = 0; i < spacesCnt; i++) { + typeText += ' '; + } + } else { + typeText = '\t'; + } + return new ReplaceCommand(selection, typeText, insertsAutoWhitespace); + } +} + +export class BaseTypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState { + + private readonly _openCharacter: string; + private readonly _closeCharacter: string; + public closeCharacterRange: Range | null; + public enclosingRange: Range | null; + + constructor(selection: Selection, text: string, lineNumberDeltaOffset: number, columnDeltaOffset: number, openCharacter: string, closeCharacter: string) { + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset); + this._openCharacter = openCharacter; + this._closeCharacter = closeCharacter; + this.closeCharacterRange = null; + this.enclosingRange = null; + } + + protected _computeCursorStateWithRange(model: ITextModel, range: Range, helper: ICursorStateComputerData): Selection { + this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn); + this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn); + return super.computeCursorState(model, helper); + } +} + +class TypeWithAutoClosingCommand extends BaseTypeWithAutoClosingCommand { + + constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) { + const text = (insertOpenCharacter ? openCharacter : '') + closeCharacter; + const lineNumberDeltaOffset = 0; + const columnDeltaOffset = -closeCharacter.length; + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter); + } + + public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + const range = inverseEditOperations[0].range; + return this._computeCursorStateWithRange(model, range, helper); + } +} + +class TypeWithIndentationAndAutoClosingCommand extends BaseTypeWithAutoClosingCommand { + + private readonly _autoIndentationEdit: { range: Range; text: string }; + private readonly _autoClosingEdit: { range: Range; text: string }; + + constructor(autoIndentationEdit: { range: Range; text: string }, selection: Selection, openCharacter: string, closeCharacter: string) { + const text = openCharacter + closeCharacter; + const lineNumberDeltaOffset = 0; + const columnDeltaOffset = openCharacter.length; + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter); + this._autoIndentationEdit = autoIndentationEdit; + this._autoClosingEdit = { range: selection, text }; + } + + public override getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + builder.addTrackedEditOperation(this._autoIndentationEdit.range, this._autoIndentationEdit.text); + builder.addTrackedEditOperation(this._autoClosingEdit.range, this._autoClosingEdit.text); + } + + public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + if (inverseEditOperations.length !== 2) { + throw new Error('There should be two inverse edit operations!'); + } + const range1 = inverseEditOperations[0].range; + const range2 = inverseEditOperations[1].range; + const range = range1.plusRange(range2); + return this._computeCursorStateWithRange(model, range, helper); + } +} + +function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { + if (typedText === ' ') { + return previousTypingOperation === EditOperationType.TypingFirstSpace + || previousTypingOperation === EditOperationType.TypingConsecutiveSpace + ? EditOperationType.TypingConsecutiveSpace + : EditOperationType.TypingFirstSpace; + } + + return EditOperationType.TypingOther; +} + +function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { + if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { + // Always set an undo stop before non-type operations + return true; + } + if (previousTypingOperation === EditOperationType.TypingFirstSpace) { + // `abc |d`: No undo stop + // `abc |d`: Undo stop + return false; + } + // Insert undo stop between different operation types + return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); +} + +function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { + return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) + ? 'space' + : type; +} + +function isTypingOperation(type: EditOperationType): boolean { + return type === EditOperationType.TypingOther + || type === EditOperationType.TypingFirstSpace + || type === EditOperationType.TypingConsecutiveSpace; +} + +function isAutoClosingOvertype(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean { + if (config.autoClosingOvertype === 'never') { + return false; + } + if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) { + return false; + } + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + if (!selection.isEmpty()) { + return false; + } + const position = selection.getPosition(); + const lineText = model.getLineContent(position.lineNumber); + const afterCharacter = lineText.charAt(position.column - 1); + if (afterCharacter !== ch) { + return false; + } + // Do not over-type quotes after a backslash + const chIsQuote = isQuote(ch); + const beforeCharacter = position.column > 2 ? lineText.charCodeAt(position.column - 2) : CharCode.Null; + if (beforeCharacter === CharCode.Backslash && chIsQuote) { + return false; + } + // Must over-type a closing character typed by the editor + if (config.autoClosingOvertype === 'auto') { + let found = false; + for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) { + const autoClosedCharacter = autoClosedCharacters[j]; + if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + } + return true; +} + +function typeCommand(range: Range, text: string, keepPosition: boolean): ICommand { + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, text, true); + } else { + return new ReplaceCommand(range, text, true); + } +} + +export function shiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { + count = count || 1; + return ShiftCommand.shiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); +} + +export function unshiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { + count = count || 1; + return ShiftCommand.unshiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); +} + +export function shouldSurroundChar(config: CursorConfiguration, ch: string): boolean { + if (isQuote(ch)) { + return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined'); + } else { + // Character is a bracket + return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined'); + } +} diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index ffa80cbb63c..b4c65156f85 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -3,26 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CharCode } from 'vs/base/common/charCode'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import * as strings from 'vs/base/common/strings'; -import { ReplaceCommand, ReplaceCommandWithOffsetCursorState, ReplaceCommandWithoutChangingPosition, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; -import { CompositionSurroundSelectionCommand, SurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; +import { CompositionSurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; import { CursorConfiguration, EditOperationResult, EditOperationType, ICursorSimpleModel, isQuote } from 'vs/editor/common/cursorCommon'; -import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { Position } from 'vs/editor/common/core/position'; -import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; +import { ICommand } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; -import { getIndentationAtPosition } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; -import { EditorAutoClosingStrategy, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; -import { getIndentActionForType, getIndentForEnter, getInheritIndentForLine } from 'vs/editor/common/languages/autoIndent'; -import { getEnterAction } from 'vs/editor/common/languages/enterAction'; +import { AutoClosingOpenCharTypeOperation, AutoClosingOvertypeOperation, AutoClosingOvertypeWithInterceptorsOperation, AutoIndentOperation, CompositionOperation, EnterOperation, InterceptorElectricCharOperation, PasteOperation, shiftIndent, shouldSurroundChar, SimpleCharacterTypeOperation, SurroundSelectionOperation, TabOperation, TypeWithoutInterceptorsOperation, unshiftIndent } from 'vs/editor/common/cursor/cursorTypeEditOperations'; export class TypeOperations { @@ -61,777 +50,23 @@ export class TypeOperations { } public static shiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { - count = count || 1; - return ShiftCommand.shiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); + return shiftIndent(config, indentation, count); } public static unshiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { - count = count || 1; - return ShiftCommand.unshiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); - } - - private static _distributedPaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string[]): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], text[i]); - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _simplePaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const position = selection.getPosition(); - - if (pasteOnNewLine && !selection.isEmpty()) { - pasteOnNewLine = false; - } - if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) { - pasteOnNewLine = false; - } - - if (pasteOnNewLine) { - // Paste entire line at the beginning of line - const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1); - commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection, true); - } else { - commands[i] = new ReplaceCommand(selection, text); - } - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _distributePasteToCursors(config: CursorConfiguration, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): string[] | null { - if (pasteOnNewLine) { - return null; - } - - if (selections.length === 1) { - return null; - } - - if (multicursorText && multicursorText.length === selections.length) { - return multicursorText; - } - - if (config.multiCursorPaste === 'spread') { - // Try to spread the pasted text in case the line count matches the cursor count - // Remove trailing \n if present - if (text.charCodeAt(text.length - 1) === CharCode.LineFeed) { - text = text.substr(0, text.length - 1); - } - // Remove trailing \r if present - if (text.charCodeAt(text.length - 1) === CharCode.CarriageReturn) { - text = text.substr(0, text.length - 1); - } - const lines = strings.splitLines(text); - if (lines.length === selections.length) { - return lines; - } - } - - return null; + return unshiftIndent(config, indentation, count); } public static paste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): EditOperationResult { - const distributedPaste = this._distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText); - - if (distributedPaste) { - selections = selections.sort(Range.compareRangesUsingStarts); - return this._distributedPaste(config, model, selections, distributedPaste); - } else { - return this._simplePaste(config, model, selections, text, pasteOnNewLine); - } - } - - private static _goodIndentForLine(config: CursorConfiguration, model: ITextModel, lineNumber: number): string | null { - let action: IndentAction | EnterAction | null = null; - let indentation: string = ''; - - const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService); - if (expectedIndentAction) { - action = expectedIndentAction.action; - indentation = expectedIndentAction.indentation; - } else if (lineNumber > 1) { - let lastLineNumber: number; - for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { - const lineText = model.getLineContent(lastLineNumber); - const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText); - if (nonWhitespaceIdx >= 0) { - break; - } - } - - if (lastLineNumber < 1) { - // No previous line with content found - return null; - } - - const maxColumn = model.getLineMaxColumn(lastLineNumber); - const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService); - if (expectedEnterAction) { - indentation = expectedEnterAction.indentation + expectedEnterAction.appendText; - } - } - - if (action) { - if (action === IndentAction.Indent) { - indentation = TypeOperations.shiftIndent(config, indentation); - } - - if (action === IndentAction.Outdent) { - indentation = TypeOperations.unshiftIndent(config, indentation); - } - - indentation = config.normalizeIndentation(indentation); - } - - if (!indentation) { - return null; - } - - return indentation; - } - - private static _replaceJumpToNextIndent(config: CursorConfiguration, model: ICursorSimpleModel, selection: Selection, insertsAutoWhitespace: boolean): ReplaceCommand { - let typeText = ''; - - const position = selection.getStartPosition(); - if (config.insertSpaces) { - const visibleColumnFromColumn = config.visibleColumnFromColumn(model, position); - const indentSize = config.indentSize; - const spacesCnt = indentSize - (visibleColumnFromColumn % indentSize); - for (let i = 0; i < spacesCnt; i++) { - typeText += ' '; - } - } else { - typeText = '\t'; - } - - return new ReplaceCommand(selection, typeText, insertsAutoWhitespace); + return PasteOperation.getEdits(config, model, selections, text, pasteOnNewLine, multicursorText); } public static tab(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - - if (selection.isEmpty()) { - - const lineText = model.getLineContent(selection.startLineNumber); - - if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { - let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); - goodIndent = goodIndent || '\t'; - const possibleTypeText = config.normalizeIndentation(goodIndent); - if (!lineText.startsWith(possibleTypeText)) { - commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true); - continue; - } - } - - commands[i] = this._replaceJumpToNextIndent(config, model, selection, true); - } else { - if (selection.startLineNumber === selection.endLineNumber) { - const lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber); - if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) { - // This is a single line selection that is not the entire line - commands[i] = this._replaceJumpToNextIndent(config, model, selection, false); - continue; - } - } - - commands[i] = new ShiftCommand(selection, { - isUnshift: false, - tabSize: config.tabSize, - indentSize: config.indentSize, - insertSpaces: config.insertSpaces, - useTabStops: config.useTabStops, - autoIndent: config.autoIndent - }, config.languageConfigurationService); - } - } - return commands; + return TabOperation.getCommands(config, model, selections); } public static compositionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): EditOperationResult { - const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), - shouldPushStackElementAfter: false - }); - } - - private static _compositionType(model: ITextModel, selection: Selection, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): ICommand | null { - if (!selection.isEmpty()) { - // looks like https://github.com/microsoft/vscode/issues/2773 - // where a cursor operation occurred before a canceled composition - // => ignore composition - return null; - } - const pos = selection.getPosition(); - const startColumn = Math.max(1, pos.column - replacePrevCharCnt); - const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt); - const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn); - const oldText = model.getValueInRange(range); - if (oldText === text && positionDelta === 0) { - // => ignore composition that doesn't do anything - return null; - } - return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta); - } - - private static _typeCommand(range: Range, text: string, keepPosition: boolean): ICommand { - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, text, true); - } else { - return new ReplaceCommand(range, text, true); - } - } - - private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { - if (config.autoIndent === EditorAutoIndentStrategy.None) { - return TypeOperations._typeCommand(range, '\n', keepPosition); - } - if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { - const lineText = model.getLineContent(range.startLineNumber); - const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); - } - - const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService); - if (r) { - if (r.indentAction === IndentAction.None) { - // Nothing special - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); - - } else if (r.indentAction === IndentAction.Indent) { - // Indent once - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); - - } else if (r.indentAction === IndentAction.IndentOutdent) { - // Ultra special - const normalIndent = config.normalizeIndentation(r.indentation); - const increasedIndent = config.normalizeIndentation(r.indentation + r.appendText); - - const typeText = '\n' + increasedIndent + '\n' + normalIndent; - - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, typeText, true); - } else { - return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); - } - } else if (r.indentAction === IndentAction.Outdent) { - const actualIndentation = TypeOperations.unshiftIndent(config, r.indentation); - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); - } - } - - const lineText = model.getLineContent(range.startLineNumber); - const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); - - if (config.autoIndent >= EditorAutoIndentStrategy.Full) { - const ir = getIndentForEnter(config.autoIndent, model, range, { - unshiftIndent: (indent) => { - return TypeOperations.unshiftIndent(config, indent); - }, - shiftIndent: (indent) => { - return TypeOperations.shiftIndent(config, indent); - }, - normalizeIndentation: (indent) => { - return config.normalizeIndentation(indent); - } - }, config.languageConfigurationService); - - if (ir) { - let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition()); - const oldEndColumn = range.endColumn; - const newLineContent = model.getLineContent(range.endLineNumber); - const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); - if (firstNonWhitespace >= 0) { - range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1)); - } else { - range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber)); - } - - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); - } else { - let offset = 0; - if (oldEndColumn <= firstNonWhitespace + 1) { - if (!config.insertSpaces) { - oldEndViewColumn = Math.ceil(oldEndViewColumn / config.indentSize); - } - offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); - } - return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); - } - } - } - - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); - } - - private static _isAutoIndentType(config: CursorConfiguration, model: ITextModel, selections: Selection[]): boolean { - if (config.autoIndent < EditorAutoIndentStrategy.Full) { - return false; - } - - for (let i = 0, len = selections.length; i < len; i++) { - if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { - return false; - } - } - - return true; - } - - private static _runAutoIndentType(config: CursorConfiguration, model: ITextModel, range: Range, ch: string): ICommand | null { - const currentIndentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn); - const actualIndentation = getIndentActionForType(config.autoIndent, model, range, ch, { - shiftIndent: (indentation) => { - return TypeOperations.shiftIndent(config, indentation); - }, - unshiftIndent: (indentation) => { - return TypeOperations.unshiftIndent(config, indentation); - }, - }, config.languageConfigurationService); - - if (actualIndentation === null) { - return null; - } - - if (actualIndentation !== config.normalizeIndentation(currentIndentation)) { - const firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(range.startLineNumber); - if (firstNonWhitespace === 0) { - return TypeOperations._typeCommand( - new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), - config.normalizeIndentation(actualIndentation) + ch, - false - ); - } else { - return TypeOperations._typeCommand( - new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), - config.normalizeIndentation(actualIndentation) + - model.getLineContent(range.startLineNumber).substring(firstNonWhitespace - 1, range.startColumn - 1) + ch, - false - ); - } - } - - return null; - } - - private static _isAutoClosingOvertype(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean { - if (config.autoClosingOvertype === 'never') { - return false; - } - - if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) { - return false; - } - - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - - if (!selection.isEmpty()) { - return false; - } - - const position = selection.getPosition(); - const lineText = model.getLineContent(position.lineNumber); - const afterCharacter = lineText.charAt(position.column - 1); - - if (afterCharacter !== ch) { - return false; - } - - // Do not over-type quotes after a backslash - const chIsQuote = isQuote(ch); - const beforeCharacter = position.column > 2 ? lineText.charCodeAt(position.column - 2) : CharCode.Null; - if (beforeCharacter === CharCode.Backslash && chIsQuote) { - return false; - } - - // Must over-type a closing character typed by the editor - if (config.autoClosingOvertype === 'auto') { - let found = false; - for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) { - const autoClosedCharacter = autoClosedCharacters[j]; - if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - } - - return true; - } - - private static _runAutoClosingOvertype(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const position = selection.getPosition(); - const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); - commands[i] = new ReplaceCommand(typeSelection, ch); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), - shouldPushStackElementAfter: false - }); - } - - private static _isBeforeClosingBrace(config: CursorConfiguration, lineAfter: string) { - // If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false - const nextChar = lineAfter.charAt(0); - const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || []; - const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || []; - - const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open)); - const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close)); - - return !isBeforeStartingBrace && isBeforeClosingBrace; - } - - /** - * Determine if typing `ch` at all `positions` in the `model` results in an - * auto closing open sequence being typed. - * - * Auto closing open sequences can consist of multiple characters, which - * can lead to ambiguities. In such a case, the longest auto-closing open - * sequence is returned. - */ - private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null { - const candidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); - if (!candidates) { - return null; - } - - // Determine which auto-closing pair it is - let result: StandardAutoClosingPairConditional | null = null; - for (const candidate of candidates) { - if (result === null || candidate.open.length > result.open.length) { - let candidateIsMatch = true; - for (const position of positions) { - const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - candidate.open.length + 1, position.lineNumber, position.column)); - if (relevantText + ch !== candidate.open) { - candidateIsMatch = false; - break; - } - } - - if (candidateIsMatch) { - result = candidate; - } - } - } - return result; - } - - /** - * Find another auto-closing pair that is contained by the one passed in. - * - * e.g. when having [(,)] and [(*,*)] as auto-closing pairs - * this method will find [(,)] as a containment pair for [(*,*)] - */ - private static _findContainedAutoClosingPair(config: CursorConfiguration, pair: StandardAutoClosingPairConditional): StandardAutoClosingPairConditional | null { - if (pair.open.length <= 1) { - return null; - } - const lastChar = pair.close.charAt(pair.close.length - 1); - // get candidates with the same last character as close - const candidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || []; - let result: StandardAutoClosingPairConditional | null = null; - for (const candidate of candidates) { - if (candidate.open !== pair.open && pair.open.includes(candidate.open) && pair.close.endsWith(candidate.close)) { - if (!result || candidate.open.length > result.open.length) { - result = candidate; - } - } - } - return result; - } - - private static _getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null { - - for (const selection of selections) { - if (!selection.isEmpty()) { - return null; - } - } - - // This method is called both when typing (regularly) and when composition ends - // This means that we need to work with a text buffer where sometimes `ch` is not - // there (it is being typed right now) or with a text buffer where `ch` has already been typed - // - // In order to avoid adding checks for `chIsAlreadyTyped` in all places, we will work - // with two conceptual positions, the position before `ch` and the position after `ch` - // - const positions: { lineNumber: number; beforeColumn: number; afterColumn: number }[] = selections.map((s) => { - const position = s.getPosition(); - if (chIsAlreadyTyped) { - return { lineNumber: position.lineNumber, beforeColumn: position.column - ch.length, afterColumn: position.column }; - } else { - return { lineNumber: position.lineNumber, beforeColumn: position.column, afterColumn: position.column }; - } - }); - - - // Find the longest auto-closing open pair in case of multiple ending in `ch` - // e.g. when having [f","] and [","], it picks [f","] if the character before is f - const pair = this._findAutoClosingPairOpen(config, model, positions.map(p => new Position(p.lineNumber, p.beforeColumn)), ch); - if (!pair) { - return null; - } - - let autoCloseConfig: EditorAutoClosingStrategy; - let shouldAutoCloseBefore: (ch: string) => boolean; - - const chIsQuote = isQuote(ch); - if (chIsQuote) { - autoCloseConfig = config.autoClosingQuotes; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.quote; - } else { - const pairIsForComments = config.blockCommentStartToken ? pair.open.includes(config.blockCommentStartToken) : false; - if (pairIsForComments) { - autoCloseConfig = config.autoClosingComments; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.comment; - } else { - autoCloseConfig = config.autoClosingBrackets; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket; - } - } - - if (autoCloseConfig === 'never') { - return null; - } - - // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship - // e.g. when having [(,)] and [(*,*)] - // - when typing (, the resulting state is (|) - // - when typing *, the desired resulting state is (*|*), not (*|*)) - const containedPair = this._findContainedAutoClosingPair(config, pair); - const containedPairClose = containedPair ? containedPair.close : ''; - let isContainedPairPresent = true; - - for (const position of positions) { - const { lineNumber, beforeColumn, afterColumn } = position; - const lineText = model.getLineContent(lineNumber); - const lineBefore = lineText.substring(0, beforeColumn - 1); - const lineAfter = lineText.substring(afterColumn - 1); - - if (!lineAfter.startsWith(containedPairClose)) { - isContainedPairPresent = false; - } - - // Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows - if (lineAfter.length > 0) { - const characterAfter = lineAfter.charAt(0); - const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, lineAfter); - - if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { - return null; - } - } - - // Do not auto-close ' or " after a word character - if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { - const wordSeparators = getMapForWordSeparators(config.wordSeparators, []); - if (lineBefore.length > 0) { - const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1); - if (wordSeparators.get(characterBefore) === WordCharacterClass.Regular) { - return null; - } - } - } - - if (!model.tokenization.isCheapToTokenize(lineNumber)) { - // Do not force tokenization - return null; - } - - model.tokenization.forceTokenization(lineNumber); - const lineTokens = model.tokenization.getLineTokens(lineNumber); - const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); - if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { - return null; - } - - // Typing for example a quote could either start a new string, in which case auto-closing is desirable - // or it could end a previously started string, in which case auto-closing is not desirable - // - // In certain cases, it is really not possible to look at the previous token to determine - // what would happen. That's why we do something really unusual, we pretend to type a different - // character and ask the tokenizer what the outcome of doing that is: after typing a neutral - // character, are we in a string (i.e. the quote would most likely end a string) or not? - // - const neutralCharacter = pair.findNeutralCharacter(); - if (neutralCharacter) { - const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); - if (!pair.isOK(tokenType)) { - return null; - } - } - } - - if (isContainedPairPresent) { - return pair.close.substring(0, pair.close.length - containedPairClose.length); - } else { - return pair.close; - } - } - - private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean, autoClosingPairClose: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - commands[i] = new TypeWithAutoClosingCommand(selection, ch, !chIsAlreadyTyped, autoClosingPairClose); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false - }); - } - - private static _shouldSurroundChar(config: CursorConfiguration, ch: string): boolean { - if (isQuote(ch)) { - return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined'); - } else { - // Character is a bracket - return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined'); - } - } - - private static _isSurroundSelectionType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { - if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { - return false; - } - - const isTypingAQuoteCharacter = isQuote(ch); - - for (const selection of selections) { - - if (selection.isEmpty()) { - return false; - } - - let selectionContainsOnlyWhitespace = true; - - for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { - const lineText = model.getLineContent(lineNumber); - const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); - const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); - const selectedText = lineText.substring(startIndex, endIndex); - if (/[^ \t]/.test(selectedText)) { - // this selected text contains something other than whitespace - selectionContainsOnlyWhitespace = false; - break; - } - } - - if (selectionContainsOnlyWhitespace) { - return false; - } - - if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) { - const selectionText = model.getValueInRange(selection); - if (isQuote(selectionText)) { - // Typing a quote character on top of another quote character - // => disable surround selection type - return false; - } - } - } - - return true; - } - - private static _runSurroundSelectionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const closeCharacter = config.surroundingPairs[ch]; - commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter); - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { - if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { - return true; - } - return false; - } - - private static _typeInterceptorElectricChar(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): EditOperationResult | null { - if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) { - return null; - } - - const position = selection.getPosition(); - model.tokenization.forceTokenization(position.lineNumber); - const lineTokens = model.tokenization.getLineTokens(position.lineNumber); - - let electricAction: IElectricAction | null; - try { - electricAction = config.onElectricCharacter(ch, lineTokens, position.column); - } catch (e) { - onUnexpectedError(e); - return null; - } - - if (!electricAction) { - return null; - } - - if (electricAction.matchOpenBracket) { - const endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; - const match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { - lineNumber: position.lineNumber, - column: endColumn - }, 500 /* give at most 500ms to compute */); - - if (match) { - if (match.startLineNumber === position.lineNumber) { - // matched something on the same line => no change in indentation - return null; - } - const matchLine = model.getLineContent(match.startLineNumber); - const matchLineIndentation = strings.getLeadingWhitespace(matchLine); - const newIndentation = config.normalizeIndentation(matchLineIndentation); - - const lineText = model.getLineContent(position.lineNumber); - const lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column; - - const prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1); - const typeText = newIndentation + prefix + ch; - - const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); - - const command = new ReplaceCommand(typeSelection, typeText); - return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { - shouldPushStackElementBefore: false, - shouldPushStackElementAfter: true - }); - } - } - - return null; + return CompositionOperation.getEdits(prevEditOperationType, config, model, selections, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta); } /** @@ -871,7 +106,7 @@ export class TypeOperations { if (hasDeletion) { // Check if this could have been a surround selection - if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { + if (!shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { return null; } @@ -914,18 +149,14 @@ export class TypeOperations { }); } - if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { - // Unfortunately, the close character is at this point "doubled", so we need to delete it... - const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false - }); + const autoClosingOvertypeEdits = AutoClosingOvertypeWithInterceptorsOperation.getEdits(config, model, selections, autoClosedCharacters, ch); + if (autoClosingOvertypeEdits !== undefined) { + return autoClosingOvertypeEdits; } - const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, true); - if (autoClosingPairClose !== null) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairClose); + const autoClosingOpenCharEdits = AutoClosingOpenCharTypeOperation.getEdits(config, model, selections, ch, true, false); + if (autoClosingOpenCharEdits !== undefined) { + return autoClosingOpenCharEdits; } return null; @@ -933,149 +164,41 @@ export class TypeOperations { public static typeWithInterceptors(isDoingComposition: boolean, prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult { - if (!isDoingComposition && ch === '\n') { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = TypeOperations._enter(config, model, false, selections[i]); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false, - }); + const enterEdits = EnterOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (enterEdits !== undefined) { + return enterEdits; } - if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) { - const commands: Array = []; - let autoIndentFails = false; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = this._runAutoIndentType(config, model, selections[i], ch); - if (!commands[i]) { - autoIndentFails = true; - break; - } - } - if (!autoIndentFails) { - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false, - }); - } + const autoIndentEdits = AutoIndentOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (autoIndentEdits !== undefined) { + return autoIndentEdits; } - if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { - return this._runAutoClosingOvertype(prevEditOperationType, config, model, selections, ch); + const autoClosingOverTypeEdits = AutoClosingOvertypeOperation.getEdits(prevEditOperationType, config, model, selections, autoClosedCharacters, ch); + if (autoClosingOverTypeEdits !== undefined) { + return autoClosingOverTypeEdits; } - if (!isDoingComposition) { - const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, false); - if (autoClosingPairClose) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairClose); - } + const autoClosingOpenCharEdits = AutoClosingOpenCharTypeOperation.getEdits(config, model, selections, ch, false, isDoingComposition); + if (autoClosingOpenCharEdits !== undefined) { + return autoClosingOpenCharEdits; } - if (!isDoingComposition && this._isSurroundSelectionType(config, model, selections, ch)) { - return this._runSurroundSelectionType(prevEditOperationType, config, model, selections, ch); + const surroundSelectionEdits = SurroundSelectionOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (surroundSelectionEdits !== undefined) { + return surroundSelectionEdits; } - // Electric characters make sense only when dealing with a single cursor, - // as multiple cursors typing brackets for example would interfer with bracket matching - if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) { - const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch); - if (r) { - return r; - } + const interceptorElectricCharOperation = InterceptorElectricCharOperation.getEdits(prevEditOperationType, config, model, selections, ch, isDoingComposition); + if (interceptorElectricCharOperation !== undefined) { + return interceptorElectricCharOperation; } - // A simple character type - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], ch); - } - - const opType = getTypingOperation(ch, prevEditOperationType); - return new EditOperationResult(opType, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), - shouldPushStackElementAfter: false - }); + return SimpleCharacterTypeOperation.getEdits(prevEditOperationType, selections, ch); } public static typeWithoutInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], str: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], str); - } - const opType = getTypingOperation(str, prevEditOperationType); - return new EditOperationResult(opType, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), - shouldPushStackElementAfter: false - }); - } - - public static lineInsertBefore(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { - if (model === null || selections === null) { - return []; - } - - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - let lineNumber = selections[i].positionLineNumber; - - if (lineNumber === 1) { - commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n'); - } else { - lineNumber--; - const column = model.getLineMaxColumn(lineNumber); - - commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); - } - } - return commands; - } - - public static lineInsertAfter(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { - if (model === null || selections === null) { - return []; - } - - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const lineNumber = selections[i].positionLineNumber; - const column = model.getLineMaxColumn(lineNumber); - commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); - } - return commands; - } - - public static lineBreakInsert(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = this._enter(config, model, true, selections[i]); - } - return commands; - } -} - -export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState { - - private readonly _openCharacter: string; - private readonly _closeCharacter: string; - public closeCharacterRange: Range | null; - public enclosingRange: Range | null; - - constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) { - super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length); - this._openCharacter = openCharacter; - this._closeCharacter = closeCharacter; - this.closeCharacterRange = null; - this.enclosingRange = null; - } - - public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { - const inverseEditOperations = helper.getInverseEditOperations(); - const range = inverseEditOperations[0].range; - this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn); - this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn); - return super.computeCursorState(model, helper); + return TypeWithoutInterceptorsOperation.getEdits(prevEditOperationType, selections, str); } } @@ -1089,40 +212,3 @@ export class CompositionOutcome { public readonly insertedSelectionEnd: number, ) { } } - -function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { - if (typedText === ' ') { - return previousTypingOperation === EditOperationType.TypingFirstSpace - || previousTypingOperation === EditOperationType.TypingConsecutiveSpace - ? EditOperationType.TypingConsecutiveSpace - : EditOperationType.TypingFirstSpace; - } - - return EditOperationType.TypingOther; -} - -function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { - if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { - // Always set an undo stop before non-type operations - return true; - } - if (previousTypingOperation === EditOperationType.TypingFirstSpace) { - // `abc |d`: No undo stop - // `abc |d`: Undo stop - return false; - } - // Insert undo stop between different operation types - return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); -} - -function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { - return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) - ? 'space' - : type; -} - -function isTypingOperation(type: EditOperationType): boolean { - return type === EditOperationType.TypingOther - || type === EditOperationType.TypingFirstSpace - || type === EditOperationType.TypingConsecutiveSpace; -} diff --git a/src/vs/editor/common/cursor/cursorWordOperations.ts b/src/vs/editor/common/cursor/cursorWordOperations.ts index b16172cc89a..a43538215b2 100644 --- a/src/vs/editor/common/cursor/cursorWordOperations.ts +++ b/src/vs/editor/common/cursor/cursorWordOperations.ts @@ -208,7 +208,7 @@ export class WordOperations { return 0; } - public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position { + public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { let lineNumber = position.lineNumber; let column = position.column; @@ -227,7 +227,8 @@ export class WordOperations { if (wordNavigationType === WordNavigationType.WordStartFast) { if ( - prevWordOnLine + !hasMulticursor // avoid having multiple cursors stop at different locations when doing word start + && prevWordOnLine && prevWordOnLine.wordType === WordType.Separator && prevWordOnLine.end - prevWordOnLine.start === 1 && prevWordOnLine.nextCharClass === WordCharacterClass.Regular @@ -830,10 +831,10 @@ export class WordPartOperations extends WordOperations { return candidates[0]; } - public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): Position { + public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, hasMulticursor: boolean): Position { const candidates = enforceDefined([ - WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordStart), - WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordEnd), + WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordStart, hasMulticursor), + WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordEnd, hasMulticursor), WordOperations._moveWordPartLeft(model, position) ]); candidates.sort(Position.compare); diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts index 50fbf66deba..2de4635030d 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts @@ -52,6 +52,18 @@ export class SequenceDiff { ); } + public static assertSorted(sequenceDiffs: SequenceDiff[]): void { + let last: SequenceDiff | undefined = undefined; + for (const cur of sequenceDiffs) { + if (last) { + if (!(last.seq1Range.endExclusive <= cur.seq1Range.start && last.seq2Range.endExclusive <= cur.seq2Range.start)) { + throw new BugIndicatingError('Sequence diffs must be sorted'); + } + } + last = cur; + } + } + constructor( public readonly seq1Range: OffsetRange, public readonly seq2Range: OffsetRange, diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts index 8f7183211d7..6a46557a177 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts @@ -9,10 +9,10 @@ import { pushMany, compareBy, numberComparator, reverseOrder } from 'vs/base/com import { MonotonousArray, findLastMonotonous } from 'vs/base/common/arraysFind'; import { SetMap } from 'vs/base/common/map'; import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; -import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; import { LineRangeFragment, isSpace } from 'vs/editor/common/diff/defaultLinesDiffComputer/utils'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm'; +import { Range } from 'vs/editor/common/core/range'; export function computeMovedLines( changes: DetailedLineRangeMapping[], @@ -260,8 +260,8 @@ function areLinesSimilar(line1: string, line2: string, timeout: ITimeout): boole const myersDiffingAlgorithm = new MyersDiffAlgorithm(); const result = myersDiffingAlgorithm.compute( - new LinesSliceCharSequence([line1], new OffsetRange(0, 1), false), - new LinesSliceCharSequence([line2], new OffsetRange(0, 1), false), + new LinesSliceCharSequence([line1], new Range(1, 1, 1, line1.length), false), + new LinesSliceCharSequence([line2], new Range(1, 1, 1, line2.length), false), timeout ); let commonNonSpaceCharCount = 0; diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts index b7c34e07604..5573f684526 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts @@ -17,7 +17,7 @@ import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShor import { LineSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/lineSequence'; import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from 'vs/editor/common/diff/linesDiffComputer'; -import { DetailedLineRangeMapping, RangeMapping } from '../rangeMapping'; +import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../rangeMapping'; export class DefaultLinesDiffComputer implements ILinesDiffComputer { private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing(); @@ -35,8 +35,8 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { new LineRange(1, modifiedLines.length + 1), [ new RangeMapping( - new Range(1, 1, originalLines.length, originalLines[0].length + 1), - new Range(1, 1, modifiedLines.length, modifiedLines[0].length + 1) + new Range(1, 1, originalLines.length, originalLines[originalLines.length - 1].length + 1), + new Range(1, 1, modifiedLines.length, modifiedLines[modifiedLines.length - 1].length + 1), ) ] ) @@ -80,7 +80,8 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { return this.myersDiffingAlgorithm.compute( sequence1, - sequence2 + sequence2, + timeout ); })(); @@ -166,7 +167,9 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { for (const ic of c.innerChanges) { const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) && validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines); - if (!valid) { return false; } + if (!valid) { + return false; + } } if (!validateRange(c.modified, modifiedLines) || !validateRange(c.original, originalLines)) { return false; @@ -207,18 +210,28 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { } private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { - const slice1 = new LinesSliceCharSequence(originalLines, diff.seq1Range, considerWhitespaceChanges); - const slice2 = new LinesSliceCharSequence(modifiedLines, diff.seq2Range, considerWhitespaceChanges); + const lineRangeMapping = toLineRangeMapping(diff); + const rangeMapping = lineRangeMapping.toRangeMapping2(originalLines, modifiedLines); + + const slice1 = new LinesSliceCharSequence(originalLines, rangeMapping.originalRange, considerWhitespaceChanges); + const slice2 = new LinesSliceCharSequence(modifiedLines, rangeMapping.modifiedRange, considerWhitespaceChanges); const diffResult = slice1.length + slice2.length < 500 ? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout) : this.myersDiffingAlgorithm.compute(slice1, slice2, timeout); + const check = false; + let diffs = diffResult.diffs; + if (check) { SequenceDiff.assertSorted(diffs); } diffs = optimizeSequenceDiffs(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } diffs = removeShortMatches(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } const result = diffs.map( (d) => @@ -228,6 +241,8 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { ) ); + if (check) { RangeMapping.assertSorted(result); } + // Assert: result applied on original should be the same as diff applied to original return { @@ -311,3 +326,10 @@ export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: s return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]); } + +function toLineRangeMapping(sequenceDiff: SequenceDiff) { + return new LineRangeMapping( + new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1), + new LineRange(sequenceDiff.seq2Range.start + 1, sequenceDiff.seq2Range.endExclusive + 1), + ); +} diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts index 9edc63d335f..7761599484b 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts @@ -13,52 +13,39 @@ import { isSpace } from 'vs/editor/common/diff/defaultLinesDiffComputer/utils'; export class LinesSliceCharSequence implements ISequence { private readonly elements: number[] = []; - private readonly firstCharOffsetByLine: number[] = []; - public readonly lineRange: OffsetRange; - // To account for trimming - private readonly additionalOffsetByLine: number[] = []; + private readonly firstElementOffsetByLineIdx: number[] = []; + private readonly lineStartOffsets: number[] = []; + private readonly trimmedWsLengthsByLineIdx: number[] = []; - constructor(public readonly lines: string[], lineRange: OffsetRange, public readonly considerWhitespaceChanges: boolean) { - // This slice has to have lineRange.length many \n! (otherwise diffing against an empty slice will be problematic) - // (Unless it covers the entire document, in that case the other slice also has to cover the entire document ands it's okay) + constructor(public readonly lines: string[], private readonly range: Range, public readonly considerWhitespaceChanges: boolean) { + this.firstElementOffsetByLineIdx.push(0); + for (let lineNumber = this.range.startLineNumber; lineNumber <= this.range.endLineNumber; lineNumber++) { + let line = lines[lineNumber - 1]; + let lineStartOffset = 0; + if (lineNumber === this.range.startLineNumber && this.range.startColumn > 1) { + lineStartOffset = this.range.startColumn - 1; + line = line.substring(lineStartOffset); + } + this.lineStartOffsets.push(lineStartOffset); - // If the slice covers the end, but does not start at the beginning, we include just the \n of the previous line. - let trimFirstLineFully = false; - if (lineRange.start > 0 && lineRange.endExclusive >= lines.length) { - lineRange = new OffsetRange(lineRange.start - 1, lineRange.endExclusive); - trimFirstLineFully = true; - } - - this.lineRange = lineRange; - - this.firstCharOffsetByLine[0] = 0; - for (let i = this.lineRange.start; i < this.lineRange.endExclusive; i++) { - let line = lines[i]; - let offset = 0; - if (trimFirstLineFully) { - offset = line.length; - line = ''; - trimFirstLineFully = false; - } else if (!considerWhitespaceChanges) { + let trimmedWsLength = 0; + if (!considerWhitespaceChanges) { const trimmedStartLine = line.trimStart(); - offset = line.length - trimmedStartLine.length; + trimmedWsLength = line.length - trimmedStartLine.length; line = trimmedStartLine.trimEnd(); } + this.trimmedWsLengthsByLineIdx.push(trimmedWsLength); - this.additionalOffsetByLine.push(offset); - - for (let i = 0; i < line.length; i++) { + const lineLength = lineNumber === this.range.endLineNumber ? Math.min(this.range.endColumn - 1 - lineStartOffset - trimmedWsLength, line.length) : line.length; + for (let i = 0; i < lineLength; i++) { this.elements.push(line.charCodeAt(i)); } - // Don't add an \n that does not exist in the document. - if (i < lines.length - 1) { + if (lineNumber < this.range.endLineNumber) { this.elements.push('\n'.charCodeAt(0)); - this.firstCharOffsetByLine[i - this.lineRange.start + 1] = this.elements.length; + this.firstElementOffsetByLineIdx.push(this.elements.length); } } - // To account for the last line - this.additionalOffsetByLine.push(0); } toString() { @@ -111,18 +98,23 @@ export class LinesSliceCharSequence implements ISequence { return score; } - public translateOffset(offset: number): Position { + public translateOffset(offset: number, preference: 'left' | 'right' = 'right'): Position { // find smallest i, so that lineBreakOffsets[i] <= offset using binary search - if (this.lineRange.isEmpty) { - return new Position(this.lineRange.start + 1, 1); - } - - const i = findLastIdxMonotonous(this.firstCharOffsetByLine, (value) => value <= offset); - return new Position(this.lineRange.start + i + 1, offset - this.firstCharOffsetByLine[i] + this.additionalOffsetByLine[i] + 1); + const i = findLastIdxMonotonous(this.firstElementOffsetByLineIdx, (value) => value <= offset); + const lineOffset = offset - this.firstElementOffsetByLineIdx[i]; + return new Position( + this.range.startLineNumber + i, + 1 + this.lineStartOffsets[i] + lineOffset + ((lineOffset === 0 && preference === 'left') ? 0 : this.trimmedWsLengthsByLineIdx[i]) + ); } public translateRange(range: OffsetRange): Range { - return Range.fromPositions(this.translateOffset(range.start), this.translateOffset(range.endExclusive)); + const pos1 = this.translateOffset(range.start, 'right'); + const pos2 = this.translateOffset(range.endExclusive, 'left'); + if (pos2.isBefore(pos1)) { + return Range.fromPositions(pos2, pos2); + } + return Range.fromPositions(pos1, pos2); } /** @@ -161,8 +153,8 @@ export class LinesSliceCharSequence implements ISequence { } public extendToFullLines(range: OffsetRange): OffsetRange { - const start = findLastMonotonous(this.firstCharOffsetByLine, x => x <= range.start) ?? 0; - const end = findFirstMonotonous(this.firstCharOffsetByLine, x => range.endExclusive <= x) ?? this.elements.length; + const start = findLastMonotonous(this.firstElementOffsetByLineIdx, x => x <= range.start) ?? 0; + const end = findFirstMonotonous(this.firstElementOffsetByLineIdx, x => range.endExclusive <= x) ?? this.elements.length; return new OffsetRange(start, end); } } diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index 810df11032f..da9c3a49109 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BugIndicatingError } from 'vs/base/common/errors'; import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { AbstractText, SingleTextEdit } from 'vs/editor/common/core/textEdit'; /** * Maps a line range in the original text model to a line range in the modified text model. @@ -85,6 +88,101 @@ export class LineRangeMapping { public get changedLineCount() { return Math.max(this.original.length, this.modified.length); } + + /** + * This method assumes that the LineRangeMapping describes a valid diff! + * I.e. if one range is empty, the other range cannot be the entire document. + * It avoids various problems when the line range points to non-existing line-numbers. + */ + public toRangeMapping(): RangeMapping { + const origInclusiveRange = this.original.toInclusiveRange(); + const modInclusiveRange = this.modified.toInclusiveRange(); + if (origInclusiveRange && modInclusiveRange) { + return new RangeMapping(origInclusiveRange, modInclusiveRange); + } else if (this.original.startLineNumber === 1 || this.modified.startLineNumber === 1) { + if (!(this.modified.startLineNumber === 1 && this.original.startLineNumber === 1)) { + // If one line range starts at 1, the other one must start at 1 as well. + throw new BugIndicatingError('not a valid diff'); + } + + // Because one range is empty and both ranges start at line 1, none of the ranges can cover all lines. + // Thus, `endLineNumberExclusive` is a valid line number. + return new RangeMapping( + new Range(this.original.startLineNumber, 1, this.original.endLineNumberExclusive, 1), + new Range(this.modified.startLineNumber, 1, this.modified.endLineNumberExclusive, 1), + ); + } else { + // We can assume here that both startLineNumbers are greater than 1. + return new RangeMapping( + new Range(this.original.startLineNumber - 1, Number.MAX_SAFE_INTEGER, this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), + new Range(this.modified.startLineNumber - 1, Number.MAX_SAFE_INTEGER, this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), + ); + } + } + + /** + * This method assumes that the LineRangeMapping describes a valid diff! + * I.e. if one range is empty, the other range cannot be the entire document. + * It avoids various problems when the line range points to non-existing line-numbers. + */ + public toRangeMapping2(original: string[], modified: string[]): RangeMapping { + if (isValidLineNumber(this.original.endLineNumberExclusive, original) + && isValidLineNumber(this.modified.endLineNumberExclusive, modified)) { + return new RangeMapping( + new Range(this.original.startLineNumber, 1, this.original.endLineNumberExclusive, 1), + new Range(this.modified.startLineNumber, 1, this.modified.endLineNumberExclusive, 1), + ); + } + + if (!this.original.isEmpty && !this.modified.isEmpty) { + return new RangeMapping( + Range.fromPositions( + new Position(this.original.startLineNumber, 1), + normalizePosition(new Position(this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), original) + ), + Range.fromPositions( + new Position(this.modified.startLineNumber, 1), + normalizePosition(new Position(this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), modified) + ), + ); + } + + if (this.original.startLineNumber > 1 && this.modified.startLineNumber > 1) { + return new RangeMapping( + Range.fromPositions( + normalizePosition(new Position(this.original.startLineNumber - 1, Number.MAX_SAFE_INTEGER), original), + normalizePosition(new Position(this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), original) + ), + Range.fromPositions( + normalizePosition(new Position(this.modified.startLineNumber - 1, Number.MAX_SAFE_INTEGER), modified), + normalizePosition(new Position(this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), modified) + ), + ); + } + + // Situation now: one range is empty and one range touches the last line and one range starts at line 1. + // I don't think this can happen. + + throw new BugIndicatingError(); + } +} + +function normalizePosition(position: Position, content: string[]): Position { + if (position.lineNumber < 1) { + return new Position(1, 1); + } + if (position.lineNumber > content.length) { + return new Position(content.length, content[content.length - 1].length + 1); + } + const line = content[position.lineNumber - 1]; + if (position.column > line.length + 1) { + return new Position(position.lineNumber, line.length + 1); + } + return position; +} + +function isValidLineNumber(lineNumber: number, lines: string[]): boolean { + return lineNumber >= 1 && lineNumber <= lines.length; } /** @@ -120,9 +218,7 @@ export class DetailedLineRangeMapping extends LineRangeMapping { } public withInnerChangesFromLineRanges(): DetailedLineRangeMapping { - return new DetailedLineRangeMapping(this.original, this.modified, [ - new RangeMapping(this.original.toExclusiveRange(), this.modified.toExclusiveRange()), - ]); + return new DetailedLineRangeMapping(this.original, this.modified, [this.toRangeMapping()]); } } @@ -130,6 +226,19 @@ export class DetailedLineRangeMapping extends LineRangeMapping { * Maps a range in the original text model to a range in the modified text model. */ export class RangeMapping { + public static assertSorted(rangeMappings: RangeMapping[]): void { + for (let i = 1; i < rangeMappings.length; i++) { + const previous = rangeMappings[i - 1]; + const current = rangeMappings[i]; + if (!( + previous.originalRange.getEndPosition().isBeforeOrEqual(current.originalRange.getStartPosition()) + && previous.modifiedRange.getEndPosition().isBeforeOrEqual(current.modifiedRange.getStartPosition()) + )) { + throw new BugIndicatingError('Range mappings must be sorted'); + } + } + } + /** * The original range. */ @@ -155,4 +264,12 @@ export class RangeMapping { public flip(): RangeMapping { return new RangeMapping(this.modifiedRange, this.originalRange); } + + /** + * Creates a single text edit that describes the change from the original to the modified text. + */ + public toTextEdit(modified: AbstractText): SingleTextEdit { + const newText = modified.getValueOfRange(this.modifiedRange); + return new SingleTextEdit(this.originalRange, newText); + } } diff --git a/src/vs/editor/common/languageFeatureRegistry.ts b/src/vs/editor/common/languageFeatureRegistry.ts index 53c14ac57b9..32489739254 100644 --- a/src/vs/editor/common/languageFeatureRegistry.ts +++ b/src/vs/editor/common/languageFeatureRegistry.ts @@ -10,10 +10,10 @@ import { LanguageFilter, LanguageSelector, score } from 'vs/editor/common/langua import { URI } from 'vs/base/common/uri'; interface Entry { - selector: LanguageSelector; - provider: T; + readonly selector: LanguageSelector; + readonly provider: T; _score: number; - _time: number; + readonly _time: number; } function isExclusive(selector: LanguageSelector): boolean { @@ -40,14 +40,16 @@ class MatchCandidate { readonly uri: URI, readonly languageId: string, readonly notebookUri: URI | undefined, - readonly notebookType: string | undefined + readonly notebookType: string | undefined, + readonly recursive: boolean, ) { } equals(other: MatchCandidate): boolean { return this.notebookType === other.notebookType && this.languageId === other.languageId && this.uri.toString() === other.uri.toString() - && this.notebookUri?.toString() === other.notebookUri?.toString(); + && this.notebookUri?.toString() === other.notebookUri?.toString() + && this.recursive === other.recursive; } } @@ -96,7 +98,7 @@ export class LanguageFeatureRegistry { return []; } - this._updateScores(model); + this._updateScores(model, false); const result: T[] = []; // from registry @@ -109,9 +111,13 @@ export class LanguageFeatureRegistry { return result; } - ordered(model: ITextModel): T[] { + allNoModel(): T[] { + return this._entries.map(entry => entry.provider); + } + + ordered(model: ITextModel, recursive = false): T[] { const result: T[] = []; - this._orderedForEach(model, entry => result.push(entry.provider)); + this._orderedForEach(model, recursive, entry => result.push(entry.provider)); return result; } @@ -120,7 +126,7 @@ export class LanguageFeatureRegistry { let lastBucket: T[]; let lastBucketScore: number; - this._orderedForEach(model, entry => { + this._orderedForEach(model, false, entry => { if (lastBucket && lastBucketScore === entry._score) { lastBucket.push(entry.provider); } else { @@ -133,9 +139,9 @@ export class LanguageFeatureRegistry { return result; } - private _orderedForEach(model: ITextModel, callback: (provider: Entry) => any): void { + private _orderedForEach(model: ITextModel, recursive: boolean, callback: (provider: Entry) => any): void { - this._updateScores(model); + this._updateScores(model, recursive); for (const entry of this._entries) { if (entry._score > 0) { @@ -146,15 +152,15 @@ export class LanguageFeatureRegistry { private _lastCandidate: MatchCandidate | undefined; - private _updateScores(model: ITextModel): void { + private _updateScores(model: ITextModel, recursive: boolean): void { const notebookInfo = this._notebookInfoResolver?.(model.uri); // use the uri (scheme, pattern) of the notebook info iff we have one // otherwise it's the model's/document's uri const candidate = notebookInfo - ? new MatchCandidate(model.uri, model.getLanguageId(), notebookInfo.uri, notebookInfo.type) - : new MatchCandidate(model.uri, model.getLanguageId(), undefined, undefined); + ? new MatchCandidate(model.uri, model.getLanguageId(), notebookInfo.uri, notebookInfo.type, recursive) + : new MatchCandidate(model.uri, model.getLanguageId(), undefined, undefined, recursive); if (this._lastCandidate?.equals(candidate)) { // nothing has changed @@ -167,13 +173,17 @@ export class LanguageFeatureRegistry { entry._score = score(entry.selector, candidate.uri, candidate.languageId, shouldSynchronizeModel(model), candidate.notebookUri, candidate.notebookType); if (isExclusive(entry.selector) && entry._score > 0) { - // support for one exclusive selector that overwrites - // any other selector - for (const entry of this._entries) { + if (recursive) { entry._score = 0; + } else { + // support for one exclusive selector that overwrites + // any other selector + for (const entry of this._entries) { + entry._score = 0; + } + entry._score = 1000; + break; } - entry._score = 1000; - break; } } diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 2c3925b4d45..f1f65bfe99f 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -9,6 +9,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { IReadonlyVSDataTransfer } from 'vs/base/common/dataTransfer'; import { Event } from 'vs/base/common/event'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -18,14 +19,13 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; +import { LanguageSelector } from 'vs/editor/common/languageSelector'; import * as model from 'vs/editor/common/model'; import { TokenizationRegistry as TokenizationRegistryImpl } from 'vs/editor/common/tokenizationRegistry'; import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IMarkerData } from 'vs/platform/markers/common/markers'; -import { LanguageFilter } from 'vs/editor/common/languageSelector'; -import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; /** * @internal @@ -82,6 +82,14 @@ export class EncodedTokenizationResult { } } +/** + * An intermediate interface for scaffolding the new tree sitter tokenization support. Not final. + * @internal + */ +export interface ITreeSitterTokenizationSupport { + name: string; +} + /** * @internal */ @@ -168,19 +176,58 @@ export interface Hover { * current position itself. */ range?: IRange; + + /** + * Can increase the verbosity of the hover + */ + canIncreaseVerbosity?: boolean; + + /** + * Can decrease the verbosity of the hover + */ + canDecreaseVerbosity?: boolean; } /** * The hover provider interface defines the contract between extensions and * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ -export interface HoverProvider { +export interface HoverProvider { /** - * Provide a hover for the given position and document. Multiple hovers at the same + * Provide a hover for the given position, context and document. Multiple hovers at the same * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. */ - provideHover(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideHover(model: model.ITextModel, position: Position, token: CancellationToken, context?: HoverContext): ProviderResult; +} + +export interface HoverContext { + /** + * Hover verbosity request + */ + verbosityRequest?: HoverVerbosityRequest; +} + +export interface HoverVerbosityRequest { + /** + * The delta by which to increase/decrease the hover verbosity level + */ + verbosityDelta: number; + /** + * The previous hover for the same position + */ + previousHover: THover; +} + +export enum HoverVerbosityAction { + /** + * Increase the verbosity of the hover + */ + Increase, + /** + * Decrease the verbosity of the hover + */ + Decrease } /** @@ -648,6 +695,11 @@ export interface InlineCompletionContext { */ readonly triggerKind: InlineCompletionTriggerKind; readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined; + /** + * @experimental + * @internal + */ + readonly userPrompt?: string | undefined; } export class SelectedSuggestionInfo { @@ -726,6 +778,12 @@ export type InlineCompletionProviderGroupId = string; export interface InlineCompletionsProvider { provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** + * @experimental + * @internal + */ + provideInlineEdits?(model: model.ITextModel, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** * Will be called when an item is shown. * @param updatedInsertText Is useful to understand bracket completion. @@ -795,6 +853,8 @@ export interface CodeActionProvider { displayName?: string; + extensionId?: string; + /** * Provide commands for the given document and range. */ @@ -1029,7 +1089,7 @@ export interface DocumentHighlightProvider { * A provider that can provide document highlights across multiple documents. */ export interface MultiDocumentHighlightProvider { - selector: LanguageFilter; + readonly selector: LanguageSelector; /** * Provide a Map of URI --> document highlights, like all occurrences of a variable or @@ -1690,13 +1750,19 @@ export enum NewSymbolNameTag { AIGenerated = 1 } +export enum NewSymbolNameTriggerKind { + Invoke = 0, + Automatic = 1, +} + export interface NewSymbolName { readonly newSymbolName: string; readonly tags?: readonly NewSymbolNameTag[]; } export interface NewSymbolNamesProvider { - provideNewSymbolNames(model: model.ITextModel, range: IRange, token: CancellationToken): ProviderResult; + supportsAutomaticNewSymbolNamesTriggerKind?: Promise; + provideNewSymbolNames(model: model.ITextModel, range: IRange, triggerKind: NewSymbolNameTriggerKind, token: CancellationToken): ProviderResult; } export interface Command { @@ -1737,9 +1803,9 @@ export interface CommentThreadTemplate { /** * @internal */ -export interface CommentInfo { +export interface CommentInfo { extensionId?: string; - threads: CommentThread[]; + threads: CommentThread[]; pendingCommentThreads?: PendingCommentThread[]; commentingRanges: CommentingRanges; } @@ -1800,6 +1866,11 @@ export interface CommentInput { uri: URI; } +export interface CommentThreadRevealOptions { + preserveFocus: boolean; + focusReply: boolean; +} + /** * @internal */ @@ -1813,7 +1884,7 @@ export interface CommentThread { range: T | undefined; label: string | undefined; contextValue: string | undefined; - comments: Comment[] | undefined; + comments: ReadonlyArray | undefined; onDidChangeComments: Event; collapsibleState?: CommentThreadCollapsibleState; initialCollapsibleState?: CommentThreadCollapsibleState; @@ -1832,6 +1903,13 @@ export interface CommentThread { isTemplate: boolean; } +/** + * @internal + */ +export interface AddedCommentThread extends CommentThread { + editorId?: string; +} + /** * @internal */ @@ -1926,7 +2004,7 @@ export interface CommentThreadChangedEvent { /** * Added comment threads. */ - readonly added: CommentThread[]; + readonly added: AddedCommentThread[]; /** * Removed comment threads. @@ -2036,14 +2114,14 @@ export interface ITokenizationSupportChangedEvent { /** * @internal */ -export interface ILazyTokenizationSupport { - get tokenizationSupport(): Promise; +export interface ILazyTokenizationSupport { + get tokenizationSupport(): Promise; } /** * @internal */ -export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSupport { +export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSupport { private _tokenizationSupport: Promise | null = null; constructor(private readonly createSupport: () => Promise) { @@ -2070,7 +2148,7 @@ export class LazyTokenizationSupport implements IDisposable, ILazyTokenizationSu /** * @internal */ -export interface ITokenizationRegistry { +export interface ITokenizationRegistry { /** * An event triggered when: @@ -2088,24 +2166,24 @@ export interface ITokenizationRegistry { /** * Register a tokenization support. */ - register(languageId: string, support: ITokenizationSupport): IDisposable; + register(languageId: string, support: TSupport): IDisposable; /** * Register a tokenization support factory. */ - registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable; + registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable; /** * Get or create the tokenization support for a language. * Returns `null` if not found. */ - getOrCreate(languageId: string): Promise; + getOrCreate(languageId: string): Promise; /** * Get the tokenization support for a language. * Returns `null` if not found. */ - get(languageId: string): ITokenizationSupport | null; + get(languageId: string): TSupport | null; /** * Returns false if a factory is still pending. @@ -2125,8 +2203,12 @@ export interface ITokenizationRegistry { /** * @internal */ -export const TokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); +export const TokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); +/** + * @internal + */ +export const TreeSitterTokenizationRegistry: ITokenizationRegistry = new TokenizationRegistryImpl(); /** * @internal @@ -2146,7 +2228,7 @@ export type DropYieldTo = { readonly kind: HierarchicalKind } | { readonly mimeT /** * @internal */ -export interface DocumentOnDropEdit { +export interface DocumentDropEdit { readonly title: string; readonly kind: HierarchicalKind | undefined; readonly handledMimeType?: string; @@ -2158,11 +2240,20 @@ export interface DocumentOnDropEdit { /** * @internal */ -export interface DocumentOnDropEditProvider { +export interface DocumentDropEditsSession { + edits: readonly DocumentDropEdit[]; + dispose(): void; +} + +/** + * @internal + */ +export interface DocumentDropEditProvider { readonly id?: string; readonly dropMimeTypes?: readonly string[]; - provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; + provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; + resolveDocumentDropEdit?(edit: DocumentDropEdit, token: CancellationToken): Promise; } export interface DocumentContextItem { @@ -2177,13 +2268,17 @@ export interface MappedEditsContext { } export interface MappedEditsProvider { + /** + * @internal + */ + readonly displayName: string; // internal /** * Provider maps code blocks from the chat into a workspace edit. * * @param document The document to provide mapped edits for. * @param codeBlocks Code blocks that come from an LLM's reply. - * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. * @param context The context for providing mapped edits. * @param token A cancellation token. * @returns A provider result of text edits. diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts index 9ae3df974aa..5c643b4fa60 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -7,17 +7,19 @@ import * as strings from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IndentAction } from 'vs/editor/common/languages/languageConfiguration'; -import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; -import { IndentConsts, IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; +import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { IndentationContextProcessor, isLanguageDifferentFromLineStart, ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; +import { CursorConfiguration } from 'vs/editor/common/cursorCommon'; export interface IVirtualModel { tokenization: { - getLineTokens(lineNumber: number): LineTokens; + getLineTokens(lineNumber: number): IViewLineTokens; getLanguageId(): string; getLanguageIdAtPosition(lineNumber: number, column: number): string; + forceTokenization?(lineNumber: number): void; }; getLineContent(lineNumber: number): string; } @@ -35,7 +37,7 @@ export interface IIndentConverter { * 0: every line above are invalid * else: nearest preceding line of the same language */ -function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentRulesSupport: IndentRulesSupport) { +function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, processedIndentRulesSupport: ProcessedIndentRulesSupport) { const languageId = model.tokenization.getLanguageIdAtPosition(lineNumber, 0); if (lineNumber > 1) { let lastLineNumber: number; @@ -46,7 +48,7 @@ function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentR return resultLineNumber; } const text = model.getLineContent(lastLineNumber); - if (indentRulesSupport.shouldIgnore(text) || /^\s+$/.test(text) || text === '') { + if (processedIndentRulesSupport.shouldIgnore(lastLineNumber) || /^\s+$/.test(text) || text === '') { resultLineNumber = lastLineNumber; continue; } @@ -85,6 +87,7 @@ export function getInheritIndentForLine( if (!indentRulesSupport) { return null; } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentRulesSupport, languageConfigurationService); if (lineNumber <= 1) { return { @@ -106,7 +109,7 @@ export function getInheritIndentForLine( } } - const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, indentRulesSupport); + const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, processedIndentRulesSupport); if (precedingUnIgnoredLine < 0) { return null; } else if (precedingUnIgnoredLine < 1) { @@ -116,14 +119,15 @@ export function getInheritIndentForLine( }; } - const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); - if (indentRulesSupport.shouldIncrease(precedingUnIgnoredLineContent) || indentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLineContent)) { + if (processedIndentRulesSupport.shouldIncrease(precedingUnIgnoredLine) || processedIndentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLine)) { + const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: IndentAction.Indent, line: precedingUnIgnoredLine }; - } else if (indentRulesSupport.shouldDecrease(precedingUnIgnoredLineContent)) { + } else if (processedIndentRulesSupport.shouldDecrease(precedingUnIgnoredLine)) { + const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: null, @@ -150,7 +154,7 @@ export function getInheritIndentForLine( (previousLineIndentMetadata & IndentConsts.INDENT_NEXTLINE_MASK)) { let stopLine = 0; for (let i = previousLine - 1; i > 0; i--) { - if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { + if (processedIndentRulesSupport.shouldIndentNextLine(i)) { continue; } stopLine = i; @@ -173,17 +177,16 @@ export function getInheritIndentForLine( } else { // search from precedingUnIgnoredLine until we find one whose indent is not temporary for (let i = precedingUnIgnoredLine; i > 0; i--) { - const lineContent = model.getLineContent(i); - if (indentRulesSupport.shouldIncrease(lineContent)) { + if (processedIndentRulesSupport.shouldIncrease(i)) { return { - indentation: strings.getLeadingWhitespace(lineContent), + indentation: strings.getLeadingWhitespace(model.getLineContent(i)), action: IndentAction.Indent, line: i }; - } else if (indentRulesSupport.shouldIndentNextLine(lineContent)) { + } else if (processedIndentRulesSupport.shouldIndentNextLine(i)) { let stopLine = 0; for (let j = i - 1; j > 0; j--) { - if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { + if (processedIndentRulesSupport.shouldIndentNextLine(i)) { continue; } stopLine = j; @@ -195,9 +198,9 @@ export function getInheritIndentForLine( action: null, line: stopLine + 1 }; - } else if (indentRulesSupport.shouldDecrease(lineContent)) { + } else if (processedIndentRulesSupport.shouldDecrease(i)) { return { - indentation: strings.getLeadingWhitespace(lineContent), + indentation: strings.getLeadingWhitespace(model.getLineContent(i)), action: null, line: i }; @@ -235,8 +238,8 @@ export function getGoodIndentForLine( return null; } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(virtualModel, indentRulesSupport, languageConfigurationService); const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService); - const lineContent = virtualModel.getLineContent(lineNumber); if (indent) { const inheritLine = indent.line; @@ -268,7 +271,7 @@ export function getGoodIndentForLine( indentation = indentConverter.unshiftIndent(indentation); } - if (indentRulesSupport.shouldDecrease(lineContent)) { + if (processedIndentRulesSupport.shouldDecrease(lineNumber)) { indentation = indentConverter.unshiftIndent(indentation); } @@ -281,7 +284,7 @@ export function getGoodIndentForLine( } } - if (indentRulesSupport.shouldDecrease(lineContent)) { + if (processedIndentRulesSupport.shouldDecrease(lineNumber)) { if (indent.action === IndentAction.Indent) { return indent.indentation; } else { @@ -308,80 +311,44 @@ export function getIndentForEnter( if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } - model.tokenization.forceTokenization(range.startLineNumber); - const lineTokens = model.tokenization.getLineTokens(range.startLineNumber); - const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); - const scopedLineText = scopedLineTokens.getLineContent(); - - let embeddedLanguage = false; - let beforeEnterText: string; - if (scopedLineTokens.firstCharOffset > 0 && lineTokens.getLanguageId(0) !== scopedLineTokens.languageId) { - // we are in the embeded language content - embeddedLanguage = true; // if embeddedLanguage is true, then we don't touch the indentation of current line - beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - beforeEnterText = lineTokens.getLineContent().substring(0, range.startColumn - 1); - } - - let afterEnterText: string; - if (range.isEmpty()) { - afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } - - const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; + const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn); + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; if (!indentRulesSupport) { return null; } - const beforeEnterResult = beforeEnterText; - const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterText); + model.tokenization.forceTokenization(range.startLineNumber); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range); + const afterEnterProcessedTokens = processedContextTokens.afterRangeProcessedTokens; + const beforeEnterProcessedTokens = processedContextTokens.beforeRangeProcessedTokens; + const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterProcessedTokens.getLineContent()); - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - return model.tokenization.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); - }, - }, - getLineContent: (lineNumber: number) => { - if (lineNumber === range.startLineNumber) { - return beforeEnterResult; - } else { - return model.getLineContent(lineNumber); - } - } - }; - - const currentLineIndent = strings.getLeadingWhitespace(lineTokens.getLineContent()); + const virtualModel = createVirtualModelWithModifiedTokensAtLine(model, range.startLineNumber, beforeEnterProcessedTokens); + const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition()); + const currentLine = model.getLineContent(range.startLineNumber); + const currentLineIndent = strings.getLeadingWhitespace(currentLine); const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService); if (!afterEnterAction) { - const beforeEnter = embeddedLanguage ? currentLineIndent : beforeEnterIndent; + const beforeEnter = languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent; return { beforeEnter: beforeEnter, afterEnter: beforeEnter }; } - let afterEnterIndent = embeddedLanguage ? currentLineIndent : afterEnterAction.indentation; + let afterEnterIndent = languageIsDifferentFromLineStart ? currentLineIndent : afterEnterAction.indentation; if (afterEnterAction.action === IndentAction.Indent) { afterEnterIndent = indentConverter.shiftIndent(afterEnterIndent); } - if (indentRulesSupport.shouldDecrease(afterEnterText)) { + if (indentRulesSupport.shouldDecrease(afterEnterProcessedTokens.getLineContent())) { afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent); } return { - beforeEnter: embeddedLanguage ? currentLineIndent : beforeEnterIndent, + beforeEnter: languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent, afterEnter: afterEnterIndent }; } @@ -391,43 +358,39 @@ export function getIndentForEnter( * this line doesn't match decreaseIndentPattern, we should not adjust the indentation. */ export function getIndentActionForType( - autoIndent: EditorAutoIndentStrategy, + cursorConfig: CursorConfiguration, model: ITextModel, range: Range, ch: string, indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): string | null { + const autoIndent = cursorConfig.autoIndent; if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } - const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); - - if (scopedLineTokens.firstCharOffset) { + const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition()); + if (languageIsDifferentFromLineStart) { // this line has mixed languages and indentation rules will not work return null; } - const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; + const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn); + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; if (!indentRulesSupport) { return null; } - const scopedLineText = scopedLineTokens.getLineContent(); - const beforeTypeText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - - // selection support - let afterTypeText: string; - if (range.isEmpty()) { - afterTypeText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterTypeText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range); + const beforeRangeText = processedContextTokens.beforeRangeProcessedTokens.getLineContent(); + const afterRangeText = processedContextTokens.afterRangeProcessedTokens.getLineContent(); + const textAroundRange = beforeRangeText + afterRangeText; + const textAroundRangeWithCharacter = beforeRangeText + ch + afterRangeText; // If previous content already matches decreaseIndentPattern, it means indentation of this line should already be adjusted // Users might change the indentation by purpose and we should honor that instead of readjusting. - if (!indentRulesSupport.shouldDecrease(beforeTypeText + afterTypeText) && indentRulesSupport.shouldDecrease(beforeTypeText + ch + afterTypeText)) { + if (!indentRulesSupport.shouldDecrease(textAroundRange) && indentRulesSupport.shouldDecrease(textAroundRangeWithCharacter)) { // after typing `ch`, the content matches decreaseIndentPattern, we should adjust the indent to a good manner. // 1. Get inherited indent action const r = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService); @@ -443,6 +406,29 @@ export function getIndentActionForType( return indentation; } + const previousLineNumber = range.startLineNumber - 1; + if (previousLineNumber > 0) { + const previousLine = model.getLineContent(previousLineNumber); + if (indentRulesSupport.shouldIndentNextLine(previousLine) && indentRulesSupport.shouldIncrease(textAroundRangeWithCharacter)) { + const inheritedIndentationData = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService); + const inheritedIndentation = inheritedIndentationData?.indentation; + if (inheritedIndentation !== undefined) { + const currentLine = model.getLineContent(range.startLineNumber); + const actualCurrentIndentation = strings.getLeadingWhitespace(currentLine); + const inferredCurrentIndentation = indentConverter.shiftIndent(inheritedIndentation); + // If the inferred current indentation is not equal to the actual current indentation, then the indentation has been intentionally changed, in that case keep it + const inferredIndentationEqualsActual = inferredCurrentIndentation === actualCurrentIndentation; + const textAroundRangeContainsOnlyWhitespace = /^\s*$/.test(textAroundRange); + const autoClosingPairs = cursorConfig.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); + const autoClosingPairExists = autoClosingPairs && autoClosingPairs.length > 0; + const isChFirstNonWhitespaceCharacterAndInAutoClosingPair = autoClosingPairExists && textAroundRangeContainsOnlyWhitespace; + if (inferredIndentationEqualsActual && isChFirstNonWhitespaceCharacterAndInAutoClosingPair) { + return inheritedIndentation; + } + } + } + } + return null; } @@ -460,3 +446,32 @@ export function getIndentMetadata( } return indentRulesSupport.getIndentMetadata(model.getLineContent(lineNumber)); } + +function createVirtualModelWithModifiedTokensAtLine(model: ITextModel, modifiedLineNumber: number, modifiedTokens: IViewLineTokens): IVirtualModel { + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number): IViewLineTokens => { + if (lineNumber === modifiedLineNumber) { + return modifiedTokens; + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId: (): string => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number): string => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, + }, + getLineContent: (lineNumber: number): string => { + if (lineNumber === modifiedLineNumber) { + return modifiedTokens.getLineContent(); + } else { + return model.getLineContent(lineNumber); + } + } + }; + return virtualModel; +} + diff --git a/src/vs/editor/common/languages/enterAction.ts b/src/vs/editor/common/languages/enterAction.ts index 447665fe816..27669db6ebe 100644 --- a/src/vs/editor/common/languages/enterAction.ts +++ b/src/vs/editor/common/languages/enterAction.ts @@ -7,7 +7,8 @@ import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IndentAction, CompleteEnterAction } from 'vs/editor/common/languages/languageConfiguration'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { getIndentationAtPosition, getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { getIndentationAtPosition, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IndentationContextProcessor } from 'vs/editor/common/languages/supports/indentationLineProcessor'; export function getEnterAction( autoIndent: EditorAutoIndentStrategy, @@ -15,33 +16,17 @@ export function getEnterAction( range: Range, languageConfigurationService: ILanguageConfigurationService ): CompleteEnterAction | null { - const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); - const richEditSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId); + model.tokenization.forceTokenization(range.startLineNumber); + const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn); + const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId); if (!richEditSupport) { return null; } - - const scopedLineText = scopedLineTokens.getLineContent(); - const beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - - // selection support - let afterEnterText: string; - if (range.isEmpty()) { - afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } - - let previousLineText = ''; - if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) { - // This is not the first line and the entire line belongs to this mode - const oneLineAboveScopedLineTokens = getScopedLineTokens(model, range.startLineNumber - 1); - if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) { - // The line above ends with text belonging to the same mode - previousLineText = oneLineAboveScopedLineTokens.getLineContent(); - } - } + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range); + const previousLineText = processedContextTokens.previousLineProcessedTokens.getLineContent(); + const beforeEnterText = processedContextTokens.beforeRangeProcessedTokens.getLineContent(); + const afterEnterText = processedContextTokens.afterRangeProcessedTokens.getLineContent(); const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); if (!enterResult) { diff --git a/src/vs/editor/common/languages/languageConfigurationRegistry.ts b/src/vs/editor/common/languages/languageConfigurationRegistry.ts index d8598afe6fd..7ff5ddec8a6 100644 --- a/src/vs/editor/common/languages/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/languages/languageConfigurationRegistry.ts @@ -9,7 +9,6 @@ import * as strings from 'vs/base/common/strings'; import { ITextModel } from 'vs/editor/common/model'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/core/wordHelper'; import { EnterAction, FoldingRules, IAutoClosingPair, IndentationRule, LanguageConfiguration, AutoClosingPairs, CharacterPair, ExplicitLanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; -import { createScopedLineTokens, ScopedLineTokens } from 'vs/editor/common/languages/supports'; import { CharacterPairSupport } from 'vs/editor/common/languages/supports/characterPair'; import { BracketElectricCharacterSupport } from 'vs/editor/common/languages/supports/electricCharacter'; import { IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; @@ -181,13 +180,6 @@ export function getIndentationAtPosition(model: ITextModel, lineNumber: number, return indentation; } -export function getScopedLineTokens(model: ITextModel, lineNumber: number, columnNumber?: number): ScopedLineTokens { - model.tokenization.forceTokenization(lineNumber); - const lineTokens = model.tokenization.getLineTokens(lineNumber); - const column = (typeof columnNumber === 'undefined' ? model.getLineMaxColumn(lineNumber) - 1 : columnNumber - 1); - return createScopedLineTokens(lineTokens, column); -} - class ComposedLanguageConfiguration { private readonly _entries: LanguageConfigurationContribution[]; private _order: number; diff --git a/src/vs/editor/common/languages/supports.ts b/src/vs/editor/common/languages/supports.ts index 8fdfa17bb51..730fa2a1b73 100644 --- a/src/vs/editor/common/languages/supports.ts +++ b/src/vs/editor/common/languages/supports.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { IViewLineTokens, LineTokens } from 'vs/editor/common/tokens/lineTokens'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageIdCodec } from 'vs/editor/common/languages'; export function createScopedLineTokens(context: LineTokens, offset: number): ScopedLineTokens { const tokenCount = context.getCount(); @@ -34,6 +35,7 @@ export function createScopedLineTokens(context: LineTokens, offset: number): Sco export class ScopedLineTokens { _scopedLineTokensBrand: void = undefined; + public readonly languageIdCodec: ILanguageIdCodec; public readonly languageId: string; private readonly _actual: LineTokens; private readonly _firstTokenIndex: number; @@ -55,6 +57,7 @@ export class ScopedLineTokens { this._lastTokenIndex = lastTokenIndex; this.firstCharOffset = firstCharOffset; this._lastCharOffset = lastCharOffset; + this.languageIdCodec = actual.languageIdCodec; } public getLineContent(): string { @@ -62,6 +65,10 @@ export class ScopedLineTokens { return actualLineContent.substring(this.firstCharOffset, this._lastCharOffset); } + public getLineLength(): number { + return this._lastCharOffset - this.firstCharOffset; + } + public getActualLineContentBefore(offset: number): string { const actualLineContent = this._actual.getLineContent(); return actualLineContent.substring(0, this.firstCharOffset + offset); @@ -78,6 +85,10 @@ export class ScopedLineTokens { public getStandardTokenType(tokenIndex: number): StandardTokenType { return this._actual.getStandardTokenType(tokenIndex + this._firstTokenIndex); } + + public toIViewLineTokens(): IViewLineTokens { + return this._actual.sliceAndInflate(this.firstCharOffset, this._lastCharOffset, 0); + } } const enum IgnoreBracketsInTokens { diff --git a/src/vs/editor/common/languages/supports/indentationLineProcessor.ts b/src/vs/editor/common/languages/supports/indentationLineProcessor.ts new file mode 100644 index 00000000000..919cb3cd4c8 --- /dev/null +++ b/src/vs/editor/common/languages/supports/indentationLineProcessor.ts @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as strings from 'vs/base/common/strings'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { createScopedLineTokens, ScopedLineTokens } from 'vs/editor/common/languages/supports'; +import { IVirtualModel } from 'vs/editor/common/languages/autoIndent'; +import { IViewLineTokens, LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { Position } from 'vs/editor/common/core/position'; + +/** + * This class is a wrapper class around {@link IndentRulesSupport}. + * It processes the lines by removing the language configuration brackets from the regex, string and comment tokens. + * It then calls into the {@link IndentRulesSupport} to validate the indentation conditions. + */ +export class ProcessedIndentRulesSupport { + + private readonly _indentRulesSupport: IndentRulesSupport; + private readonly _indentationLineProcessor: IndentationLineProcessor; + + constructor( + model: IVirtualModel, + indentRulesSupport: IndentRulesSupport, + languageConfigurationService: ILanguageConfigurationService + ) { + this._indentRulesSupport = indentRulesSupport; + this._indentationLineProcessor = new IndentationLineProcessor(model, languageConfigurationService); + } + + /** + * Apply the new indentation and return whether the indentation level should be increased after the given line number + */ + public shouldIncrease(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldIncrease(processedLine); + } + + /** + * Apply the new indentation and return whether the indentation level should be decreased after the given line number + */ + public shouldDecrease(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldDecrease(processedLine); + } + + /** + * Apply the new indentation and return whether the indentation level should remain unchanged at the given line number + */ + public shouldIgnore(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldIgnore(processedLine); + } + + /** + * Apply the new indentation and return whether the indentation level should increase on the line after the given line number + */ + public shouldIndentNextLine(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldIndentNextLine(processedLine); + } + +} + +/** + * This class fetches the processed text around a range which can be used for indentation evaluation. + * It returns: + * - The processed text before the given range and on the same start line + * - The processed text after the given range and on the same end line + * - The processed text on the previous line + */ +export class IndentationContextProcessor { + + private readonly model: ITextModel; + private readonly indentationLineProcessor: IndentationLineProcessor; + + constructor( + model: ITextModel, + languageConfigurationService: ILanguageConfigurationService + ) { + this.model = model; + this.indentationLineProcessor = new IndentationLineProcessor(model, languageConfigurationService); + } + + /** + * Returns the processed text, stripped from the language configuration brackets within the string, comment and regex tokens, around the given range + */ + getProcessedTokenContextAroundRange(range: Range): { + beforeRangeProcessedTokens: IViewLineTokens; + afterRangeProcessedTokens: IViewLineTokens; + previousLineProcessedTokens: IViewLineTokens; + } { + const beforeRangeProcessedTokens = this._getProcessedTokensBeforeRange(range); + const afterRangeProcessedTokens = this._getProcessedTokensAfterRange(range); + const previousLineProcessedTokens = this._getProcessedPreviousLineTokens(range); + return { beforeRangeProcessedTokens, afterRangeProcessedTokens, previousLineProcessedTokens }; + } + + private _getProcessedTokensBeforeRange(range: Range): IViewLineTokens { + this.model.tokenization.forceTokenization(range.startLineNumber); + const lineTokens = this.model.tokenization.getLineTokens(range.startLineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); + let slicedTokens: IViewLineTokens; + if (isLanguageDifferentFromLineStart(this.model, range.getStartPosition())) { + const columnIndexWithinScope = (range.startColumn - 1) - scopedLineTokens.firstCharOffset; + const firstCharacterOffset = scopedLineTokens.firstCharOffset; + const lastCharacterOffset = firstCharacterOffset + columnIndexWithinScope; + slicedTokens = lineTokens.sliceAndInflate(firstCharacterOffset, lastCharacterOffset, 0); + } else { + const columnWithinLine = range.startColumn - 1; + slicedTokens = lineTokens.sliceAndInflate(0, columnWithinLine, 0); + } + const processedTokens = this.indentationLineProcessor.getProcessedTokens(slicedTokens); + return processedTokens; + } + + private _getProcessedTokensAfterRange(range: Range): IViewLineTokens { + const position: Position = range.isEmpty() ? range.getStartPosition() : range.getEndPosition(); + this.model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = this.model.tokenization.getLineTokens(position.lineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, position.column - 1); + const columnIndexWithinScope = position.column - 1 - scopedLineTokens.firstCharOffset; + const firstCharacterOffset = scopedLineTokens.firstCharOffset + columnIndexWithinScope; + const lastCharacterOffset = scopedLineTokens.firstCharOffset + scopedLineTokens.getLineLength(); + const slicedTokens = lineTokens.sliceAndInflate(firstCharacterOffset, lastCharacterOffset, 0); + const processedTokens = this.indentationLineProcessor.getProcessedTokens(slicedTokens); + return processedTokens; + } + + private _getProcessedPreviousLineTokens(range: Range): IViewLineTokens { + const getScopedLineTokensAtEndColumnOfLine = (lineNumber: number): ScopedLineTokens => { + this.model.tokenization.forceTokenization(lineNumber); + const lineTokens = this.model.tokenization.getLineTokens(lineNumber); + const endColumnOfLine = this.model.getLineMaxColumn(lineNumber) - 1; + const scopedLineTokensAtEndColumn = createScopedLineTokens(lineTokens, endColumnOfLine); + return scopedLineTokensAtEndColumn; + }; + + this.model.tokenization.forceTokenization(range.startLineNumber); + const lineTokens = this.model.tokenization.getLineTokens(range.startLineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); + const emptyTokens = LineTokens.createEmpty('', scopedLineTokens.languageIdCodec); + const previousLineNumber = range.startLineNumber - 1; + const isFirstLine = previousLineNumber === 0; + if (isFirstLine) { + return emptyTokens; + } + const canScopeExtendOnPreviousLine = scopedLineTokens.firstCharOffset === 0; + if (!canScopeExtendOnPreviousLine) { + return emptyTokens; + } + const scopedLineTokensAtEndColumnOfPreviousLine = getScopedLineTokensAtEndColumnOfLine(previousLineNumber); + const doesLanguageContinueOnPreviousLine = scopedLineTokens.languageId === scopedLineTokensAtEndColumnOfPreviousLine.languageId; + if (!doesLanguageContinueOnPreviousLine) { + return emptyTokens; + } + const previousSlicedLineTokens = scopedLineTokensAtEndColumnOfPreviousLine.toIViewLineTokens(); + const processedTokens = this.indentationLineProcessor.getProcessedTokens(previousSlicedLineTokens); + return processedTokens; + } +} + +/** + * This class performs the actual processing of the indentation lines. + * The brackets of the language configuration are removed from the regex, string and comment tokens. + */ +class IndentationLineProcessor { + + constructor( + private readonly model: IVirtualModel, + private readonly languageConfigurationService: ILanguageConfigurationService + ) { } + + /** + * Get the processed line for the given line number and potentially adjust the indentation level. + * Remove the language configuration brackets from the regex, string and comment tokens. + */ + getProcessedLine(lineNumber: number, newIndentation?: string): string { + const replaceIndentation = (line: string, newIndentation: string): string => { + const currentIndentation = strings.getLeadingWhitespace(line); + const adjustedLine = newIndentation + line.substring(currentIndentation.length); + return adjustedLine; + }; + + this.model.tokenization.forceTokenization?.(lineNumber); + const tokens = this.model.tokenization.getLineTokens(lineNumber); + let processedLine = this.getProcessedTokens(tokens).getLineContent(); + if (newIndentation !== undefined) { + processedLine = replaceIndentation(processedLine, newIndentation); + } + return processedLine; + } + + /** + * Process the line with the given tokens, remove the language configuration brackets from the regex, string and comment tokens. + */ + getProcessedTokens(tokens: IViewLineTokens): IViewLineTokens { + + const shouldRemoveBracketsFromTokenType = (tokenType: StandardTokenType): boolean => { + return tokenType === StandardTokenType.String + || tokenType === StandardTokenType.RegEx + || tokenType === StandardTokenType.Comment; + }; + + const languageId = tokens.getLanguageId(0); + const bracketsConfiguration = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew; + const bracketsRegExp = bracketsConfiguration.getBracketRegExp({ global: true }); + const textAndMetadata: { text: string; metadata: number }[] = []; + tokens.forEach((tokenIndex: number) => { + const tokenType = tokens.getStandardTokenType(tokenIndex); + let text = tokens.getTokenText(tokenIndex); + if (shouldRemoveBracketsFromTokenType(tokenType)) { + text = text.replace(bracketsRegExp, ''); + } + const metadata = tokens.getMetadata(tokenIndex); + textAndMetadata.push({ text, metadata }); + }); + const processedLineTokens = LineTokens.createFromTextAndMetadata(textAndMetadata, tokens.languageIdCodec); + return processedLineTokens; + } +} + +export function isLanguageDifferentFromLineStart(model: ITextModel, position: Position): boolean { + model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, position.column - 1); + const doesScopeStartAtOffsetZero = scopedLineTokens.firstCharOffset === 0; + const isScopedLanguageEqualToFirstLanguageOnLine = lineTokens.getLanguageId(0) === scopedLineTokens.languageId; + const languageIsDifferentFromLineStart = !doesScopeStartAtOffsetZero && !isScopedLanguageEqualToFirstLanguageOnLine; + return languageIsDifferentFromLineStart; +} diff --git a/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts index a989e2f35e4..4989395b264 100644 --- a/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts +++ b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { CachedFunction } from 'vs/base/common/cache'; +import { RegExpOptions } from 'vs/base/common/strings'; import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; +import { createBracketOrRegExp } from 'vs/editor/common/languages/supports/richEditBrackets'; /** * Captures all bracket related configurations for a single language. @@ -91,6 +93,11 @@ export class LanguageBracketsConfiguration { public getBracketInfo(bracketText: string): BracketKind | undefined { return this.getOpeningBracketInfo(bracketText) || this.getClosingBracketInfo(bracketText); } + + public getBracketRegExp(options?: RegExpOptions): RegExp { + const brackets = Array.from([...this._openingBrackets.keys(), ...this._closingBrackets.keys()]); + return createBracketOrRegExp(brackets, options); + } } function filterValidBrackets(bracketPairs: [string, string][]): [string, string][] { diff --git a/src/vs/editor/common/languages/supports/richEditBrackets.ts b/src/vs/editor/common/languages/supports/richEditBrackets.ts index c6efd4ee7a7..abb30850466 100644 --- a/src/vs/editor/common/languages/supports/richEditBrackets.ts +++ b/src/vs/editor/common/languages/supports/richEditBrackets.ts @@ -408,9 +408,9 @@ function prepareBracketForRegExp(str: string): string { return (insertWordBoundaries ? `\\b${str}\\b` : str); } -function createBracketOrRegExp(pieces: string[]): RegExp { +export function createBracketOrRegExp(pieces: string[], options?: strings.RegExpOptions): RegExp { const regexStr = `(${pieces.map(prepareBracketForRegExp).join(')|(')})`; - return strings.createRegExp(regexStr, true); + return strings.createRegExp(regexStr, true, options); } const toReversedString = (function () { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index e21aa7d600c..134234bbfe5 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1431,6 +1431,11 @@ export interface IReadonlyTextBuffer { getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; + + /** + * Get nearest chunk of text after `offset` in the text buffer. + */ + getNearestChunk(offset: number): string; } /** diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts index 470a16f009c..3d3fe2e3649 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts @@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ignoreBracketsInToken } from 'vs/editor/common/languages/supports'; import { LanguageBracketsConfiguration } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/common/languages/supports/richEditBrackets'; @@ -36,19 +36,17 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai private readonly languageConfigurationService: ILanguageConfigurationService ) { super(); - - this._register( - this.languageConfigurationService.onDidChange(e => { - if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { - this.bracketPairsTree.clear(); - this.updateBracketPairsTree(); - } - }) - ); } //#region TextModel events + public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void { + if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); + } + } + public handleDidChangeOptions(e: IModelOptionsChangedEvent): void { this.bracketPairsTree.clear(); this.updateBracketPairsTree(); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index b75d0d75a70..24f90651f95 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -666,6 +666,27 @@ export class PieceTreeBase { return this._getCharCode(nodePos); } + public getNearestChunk(offset: number): string { + const nodePos = this.nodeAt(offset); + if (nodePos.remainder === nodePos.node.piece.length) { + // the offset is at the head of next node. + const matchingNode = nodePos.node.next(); + if (!matchingNode || matchingNode === SENTINEL) { + return ''; + } + + const buffer = this._buffers[matchingNode.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); + return buffer.buffer.substring(startOffset, startOffset + matchingNode.piece.length); + } else { + const buffer = this._buffers[nodePos.node.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); + const targetOffset = startOffset + nodePos.remainder; + const targetEnd = startOffset + nodePos.node.piece.length; + return buffer.buffer.substring(targetOffset, targetEnd); + } + } + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) { const buffer = this._buffers[node.piece.bufferIndex]; const startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 12d7e0b0981..a369298c0c9 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -167,6 +167,10 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { return this.getValueLengthInRange(range, eol); } + public getNearestChunk(offset: number): string { + return this._pieceTree.getNearestChunk(offset); + } + public getLength(): number { return this._pieceTree.getLength(); } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index a3e282ba628..04f76ee800b 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -43,6 +43,7 @@ import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPai import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; @@ -245,7 +246,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _buffer: model.ITextBuffer; private _bufferDisposable: IDisposable; private _options: model.TextModelResolvedOptions; - private _languageSelectionListener = this._register(new MutableDisposable()); + private readonly _languageSelectionListener = this._register(new MutableDisposable()); private _isDisposed: boolean; private __isDisposing: boolean; @@ -299,6 +300,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @ILanguageService private readonly _languageService: ILanguageService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -327,13 +329,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._bracketPairs = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService)); this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService)); this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); - this._tokenizationTextModelPart = new TokenizationTextModelPart( - this._languageService, - this._languageConfigurationService, + this._tokenizationTextModelPart = this.instantiationService.createInstance(TokenizationTextModelPart, this, this._bracketPairs, languageId, - this._attachedViews, + this._attachedViews ); const bufferLineCount = this._buffer.getLineCount(); @@ -381,6 +381,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati })); this._languageService.requestRichLanguageFeatures(languageId); + + this._register(this._languageConfigurationService.onDidChange(e => { + this._bracketPairs.handleLanguageConfigurationServiceChange(e); + this._tokenizationTextModelPart.handleLanguageConfigurationServiceChange(e); + })); } public override dispose(): void { @@ -413,7 +418,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _assertNotDisposed(): void { if (this._isDisposed) { - throw new Error('Model is disposed!'); + throw new BugIndicatingError('Model is disposed!'); } } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 804f63c6a28..30972f1a33b 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -17,7 +17,7 @@ import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IAttachedView } from 'vs/editor/common/model'; import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; import { AttachedViews, IAttachedViewState, TextModel } from 'vs/editor/common/model/textModel'; @@ -47,21 +47,15 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz private readonly grammarTokens = this._register(new GrammarTokens(this._languageService.languageIdCodec, this._textModel, () => this._languageId, this._attachedViews)); constructor( - private readonly _languageService: ILanguageService, - private readonly _languageConfigurationService: ILanguageConfigurationService, private readonly _textModel: TextModel, private readonly _bracketPairsTextModelPart: BracketPairsTextModelPart, private _languageId: string, private readonly _attachedViews: AttachedViews, + @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(); - this._register(this._languageConfigurationService.onDidChange(e => { - if (e.affects(this._languageId)) { - this._onDidChangeLanguageConfiguration.fire({}); - } - })); - this._register(this.grammarTokens.onDidChangeTokens(e => { this._emitModelTokensChangedEvent(e); })); @@ -77,6 +71,12 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz || this._onDidChangeTokens.hasListeners()); } + public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void { + if (e.affects(this._languageId)) { + this._onDidChangeLanguageConfiguration.fire({}); + } + } + public handleDidChangeContent(e: IModelContentChangedEvent): void { if (e.isFlush) { this._semanticTokens.flush(); diff --git a/src/vs/editor/common/services/findSectionHeaders.ts b/src/vs/editor/common/services/findSectionHeaders.ts index 08bd3709741..8e03723d0dd 100644 --- a/src/vs/editor/common/services/findSectionHeaders.ts +++ b/src/vs/editor/common/services/findSectionHeaders.ts @@ -36,7 +36,7 @@ export interface SectionHeader { shouldBeInComments: boolean; } -const markRegex = /\bMARK:\s*(.*)$/d; +const markRegex = new RegExp('\\bMARK:\\s*(.*)$', 'd'); const trimDashesRegex = /^-+|-+$/g; /** diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 45678eeab0f..52a1e2633e6 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -5,7 +5,7 @@ import { Schemas } from 'vs/base/common/network'; import { DataUri } from 'vs/base/common/resources'; -import { URI as uri } from 'vs/base/common/uri'; +import { URI, URI as uri } from 'vs/base/common/uri'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IModelService } from 'vs/editor/common/services/model'; @@ -14,11 +14,15 @@ import { ThemeIcon } from 'vs/base/common/themables'; const fileIconDirectoryRegex = /(?:\/|^)(?:([^\/]+)\/)?([^\/]+)$/; -export function getIconClasses(modelService: IModelService, languageService: ILanguageService, resource: uri | undefined, fileKind?: FileKind, icon?: ThemeIcon): string[] { - if (icon) { +export function getIconClasses(modelService: IModelService, languageService: ILanguageService, resource: uri | undefined, fileKind?: FileKind, icon?: ThemeIcon | URI): string[] { + if (ThemeIcon.isThemeIcon(icon)) { return [`codicon-${icon.id}`, 'predefined-file-icon']; } + if (URI.isUri(icon)) { + return []; + } + // we always set these base classes even if we do not have a path const classes = fileKind === FileKind.ROOT_FOLDER ? ['rootfolder-icon'] : fileKind === FileKind.FOLDER ? ['folder-icon'] : ['file-icon']; if (resource) { @@ -119,5 +123,5 @@ function detectLanguageId(modelService: IModelService, languageService: ILanguag } function cssEscape(str: string): string { - return str.replace(/[\11\12\14\15\40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. + return str.replace(/[\x11\x12\x14\x15\x40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. } diff --git a/src/vs/editor/common/services/languageFeatureDebounce.ts b/src/vs/editor/common/services/languageFeatureDebounce.ts index e537b05bc82..5f82d301a61 100644 --- a/src/vs/editor/common/services/languageFeatureDebounce.ts +++ b/src/vs/editor/common/services/languageFeatureDebounce.ts @@ -134,7 +134,7 @@ export class LanguageFeatureDebounceService implements ILanguageFeatureDebounceS const key = `${IdentityHash.of(feature)},${min}${extra ? ',' + extra : ''}`; let info = this._data.get(key); if (!info) { - if (!this._isDev) { + if (this._isDev) { this._logService.debug(`[DEBOUNCE: ${name}] is disabled in developed mode`); info = new NullDebounceInformation(min * 1.5); } else { diff --git a/src/vs/editor/common/services/languageFeatures.ts b/src/vs/editor/common/services/languageFeatures.ts index 72889bd0b7e..205d1b86f10 100644 --- a/src/vs/editor/common/services/languageFeatures.ts +++ b/src/vs/editor/common/services/languageFeatures.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LanguageFeatureRegistry, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MappedEditsProvider, MultiDocumentHighlightProvider, NewSymbolNamesProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, InlineEditProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MappedEditsProvider, MultiDocumentHighlightProvider, NewSymbolNamesProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, InlineEditProvider } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const ILanguageFeaturesService = createDecorator('ILanguageFeaturesService'); @@ -75,7 +75,7 @@ export interface ILanguageFeaturesService { readonly evaluatableExpressionProvider: LanguageFeatureRegistry; - readonly documentOnDropEditProvider: LanguageFeatureRegistry; + readonly documentDropEditProvider: LanguageFeatureRegistry; readonly mappedEditsProvider: LanguageFeatureRegistry; diff --git a/src/vs/editor/common/services/languageFeaturesService.ts b/src/vs/editor/common/services/languageFeaturesService.ts index 920a78d7402..2b1384ba099 100644 --- a/src/vs/editor/common/services/languageFeaturesService.ts +++ b/src/vs/editor/common/services/languageFeaturesService.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { LanguageFeatureRegistry, NotebookInfo, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, MultiDocumentHighlightProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, MappedEditsProvider, NewSymbolNamesProvider, InlineEditProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, MultiDocumentHighlightProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, MappedEditsProvider, NewSymbolNamesProvider, InlineEditProvider } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -43,7 +43,7 @@ export class LanguageFeaturesService implements ILanguageFeaturesService { readonly evaluatableExpressionProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentRangeSemanticTokensProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentSemanticTokensProvider = new LanguageFeatureRegistry(this._score.bind(this)); - readonly documentOnDropEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); + readonly documentDropEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentPasteEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly mappedEditsProvider: LanguageFeatureRegistry = new LanguageFeatureRegistry(this._score.bind(this)); diff --git a/src/vs/editor/common/services/languageService.ts b/src/vs/editor/common/services/languageService.ts index 6d96f2a2502..56f34e0007a 100644 --- a/src/vs/editor/common/services/languageService.ts +++ b/src/vs/editor/common/services/languageService.ts @@ -11,6 +11,7 @@ import { ILanguageNameIdPair, ILanguageSelection, ILanguageService, ILanguageIco import { firstOrDefault } from 'vs/base/common/arrays'; import { ILanguageIdCodec, TokenizationRegistry } from 'vs/editor/common/languages'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IObservable, observableFromEvent } from 'vs/base/common/observable'; export class LanguageService extends Disposable implements ILanguageService { public _serviceBrand: undefined; @@ -150,51 +151,13 @@ export class LanguageService extends Disposable implements ILanguageService { } class LanguageSelection implements ILanguageSelection { + private readonly _value: IObservable; + public readonly onDidChange: Event; - public languageId: string; - - private _listener: IDisposable | null = null; - private _emitter: Emitter | null = null; - - constructor( - private readonly _onDidChangeLanguages: Event, - private readonly _selector: () => string - ) { - this.languageId = this._selector(); + constructor(onDidChangeLanguages: Event, selector: () => string) { + this._value = observableFromEvent(this, onDidChangeLanguages, () => selector()); + this.onDidChange = Event.fromObservable(this._value); } - private _dispose(): void { - if (this._listener) { - this._listener.dispose(); - this._listener = null; - } - if (this._emitter) { - this._emitter.dispose(); - this._emitter = null; - } - } - - public get onDidChange(): Event { - if (!this._listener) { - this._listener = this._onDidChangeLanguages(() => this._evaluate()); - } - if (!this._emitter) { - this._emitter = new Emitter({ - onDidRemoveLastListener: () => { - this._dispose(); - } - }); - } - return this._emitter.event; - } - - private _evaluate(): void { - const languageId = this._selector(); - if (languageId === this.languageId) { - // no change - return; - } - this.languageId = languageId; - this._emitter?.fire(this.languageId); - } + public get languageId(): string { return this._value.get(); } } diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 2bbda14a026..00019d6f961 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -14,7 +14,7 @@ import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; import { IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageSelection } from 'vs/editor/common/languages/language'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -23,7 +23,7 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { equals } from 'vs/base/common/objects'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -107,8 +107,7 @@ export class ModelService extends Disposable implements IModelService { @IConfigurationService private readonly _configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, - @ILanguageService private readonly _languageService: ILanguageService, - @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._modelCreationOptionsByLanguageAndResource = Object.create(null); @@ -314,14 +313,11 @@ export class ModelService extends Disposable implements IModelService { private _createModelData(value: string | ITextBufferFactory, languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { // create & save the model const options = this.getCreationOptions(languageIdOrSelection, resource, isForSimpleWidget); - const model: TextModel = new TextModel( + const model: TextModel = this._instantiationService.createInstance(TextModel, value, languageIdOrSelection, options, - resource, - this._undoRedoService, - this._languageService, - this._languageConfigurationService, + resource ); if (resource && this._disposedModels.has(MODEL_ID(resource))) { const disposedModelData = this._removeDisposedModel(resource)!; diff --git a/src/vs/editor/common/services/semanticTokensProviderStyling.ts b/src/vs/editor/common/services/semanticTokensProviderStyling.ts index f248e0e23c2..1bb2e0d6ed1 100644 --- a/src/vs/editor/common/services/semanticTokensProviderStyling.ts +++ b/src/vs/editor/common/services/semanticTokensProviderStyling.ts @@ -14,6 +14,8 @@ const enum SemanticTokensProviderStylingConstants { NO_STYLING = 0b01111111111111111111111111111111 } +const ENABLE_TRACE = false; + export class SemanticTokensProviderStyling { private readonly _hashTable: HashTable; @@ -36,7 +38,7 @@ export class SemanticTokensProviderStyling { let metadata: number; if (entry) { metadata = entry.metadata; - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling [CACHED] ${tokenTypeIndex} / ${tokenModifierSet}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); } } else { @@ -50,7 +52,7 @@ export class SemanticTokensProviderStyling { } modifierSet = modifierSet >> 1; } - if (modifierSet > 0 && this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && modifierSet > 0 && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling: unknown token modifier index: ${tokenModifierSet.toString(2)} for legend: ${JSON.stringify(this._legend.tokenModifiers)}`); tokenModifiers.push('not-in-legend'); } @@ -86,7 +88,7 @@ export class SemanticTokensProviderStyling { } } } else { - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling: unknown token type index: ${tokenTypeIndex} for legend: ${JSON.stringify(this._legend.tokenTypes)}`); } metadata = SemanticTokensProviderStylingConstants.NO_STYLING; @@ -94,7 +96,7 @@ export class SemanticTokensProviderStyling { } this._hashTable.add(tokenTypeIndex, tokenModifierSet, encodedLanguageId, metadata); - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling ${tokenTypeIndex} (${tokenType}) / ${tokenModifierSet} (${tokenModifiers.join(' ')}): foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); } } diff --git a/src/vs/editor/common/services/textResourceConfigurationService.ts b/src/vs/editor/common/services/textResourceConfigurationService.ts index 89acdd09e8b..39390cbd033 100644 --- a/src/vs/editor/common/services/textResourceConfigurationService.ts +++ b/src/vs/editor/common/services/textResourceConfigurationService.ts @@ -43,26 +43,8 @@ export class TextResourceConfigurationService extends Disposable implements ITex if (configurationTarget === undefined) { configurationTarget = this.deriveConfigurationTarget(configurationValue, language); } - switch (configurationTarget) { - case ConfigurationTarget.MEMORY: - return this._updateValue(key, value, configurationTarget, configurationValue.memory?.override, resource, language); - case ConfigurationTarget.WORKSPACE_FOLDER: - return this._updateValue(key, value, configurationTarget, configurationValue.workspaceFolder?.override, resource, language); - case ConfigurationTarget.WORKSPACE: - return this._updateValue(key, value, configurationTarget, configurationValue.workspace?.override, resource, language); - case ConfigurationTarget.USER_REMOTE: - return this._updateValue(key, value, configurationTarget, configurationValue.userRemote?.override, resource, language); - default: - return this._updateValue(key, value, configurationTarget, configurationValue.userLocal?.override, resource, language); - } - } - - private _updateValue(key: string, value: any, configurationTarget: ConfigurationTarget, overriddenValue: any | undefined, resource: URI, language: string | null): Promise { - if (language && overriddenValue !== undefined) { - return this.configurationService.updateValue(key, value, { resource, overrideIdentifier: language }, configurationTarget); - } else { - return this.configurationService.updateValue(key, value, { resource }, configurationTarget); - } + const overrideIdentifier = language && configurationValue.overrideIdentifiers?.includes(language) ? language : undefined; + return this.configurationService.updateValue(key, value, { resource, overrideIdentifier }, configurationTarget); } private deriveConfigurationTarget(configurationValue: IConfigurationValue, language: string | null): ConfigurationTarget { diff --git a/src/vs/editor/common/services/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitterParserService.ts new file mode 100644 index 00000000000..e3e911efc49 --- /dev/null +++ b/src/vs/editor/common/services/treeSitterParserService.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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ITreeSitterParserService = createDecorator('treeSitterParserService'); + +/** + * Currently this service just logs telemetry about how long it takes to parse files. + * Actual API will come later as we add features like syntax highlighting. + */ +export interface ITreeSitterParserService { + readonly _serviceBrand: undefined; +} diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index d01db6500b3..7cd16daae8b 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -261,68 +261,69 @@ export enum EditorOption { pasteAs = 85, parameterHints = 86, peekWidgetDefaultFocus = 87, - definitionLinkOpensInPeek = 88, - quickSuggestions = 89, - quickSuggestionsDelay = 90, - readOnly = 91, - readOnlyMessage = 92, - renameOnType = 93, - renderControlCharacters = 94, - renderFinalNewline = 95, - renderLineHighlight = 96, - renderLineHighlightOnlyWhenFocus = 97, - renderValidationDecorations = 98, - renderWhitespace = 99, - revealHorizontalRightPadding = 100, - roundedSelection = 101, - rulers = 102, - scrollbar = 103, - scrollBeyondLastColumn = 104, - scrollBeyondLastLine = 105, - scrollPredominantAxis = 106, - selectionClipboard = 107, - selectionHighlight = 108, - selectOnLineNumbers = 109, - showFoldingControls = 110, - showUnused = 111, - snippetSuggestions = 112, - smartSelect = 113, - smoothScrolling = 114, - stickyScroll = 115, - stickyTabStops = 116, - stopRenderingLineAfter = 117, - suggest = 118, - suggestFontSize = 119, - suggestLineHeight = 120, - suggestOnTriggerCharacters = 121, - suggestSelection = 122, - tabCompletion = 123, - tabIndex = 124, - unicodeHighlighting = 125, - unusualLineTerminators = 126, - useShadowDOM = 127, - useTabStops = 128, - wordBreak = 129, - wordSegmenterLocales = 130, - wordSeparators = 131, - wordWrap = 132, - wordWrapBreakAfterCharacters = 133, - wordWrapBreakBeforeCharacters = 134, - wordWrapColumn = 135, - wordWrapOverride1 = 136, - wordWrapOverride2 = 137, - wrappingIndent = 138, - wrappingStrategy = 139, - showDeprecated = 140, - inlayHints = 141, - editorClassName = 142, - pixelRatio = 143, - tabFocusMode = 144, - layoutInfo = 145, - wrappingInfo = 146, - defaultColorDecorators = 147, - colorDecoratorsActivatedOn = 148, - inlineCompletionsAccessibilityVerbose = 149 + placeholder = 88, + definitionLinkOpensInPeek = 89, + quickSuggestions = 90, + quickSuggestionsDelay = 91, + readOnly = 92, + readOnlyMessage = 93, + renameOnType = 94, + renderControlCharacters = 95, + renderFinalNewline = 96, + renderLineHighlight = 97, + renderLineHighlightOnlyWhenFocus = 98, + renderValidationDecorations = 99, + renderWhitespace = 100, + revealHorizontalRightPadding = 101, + roundedSelection = 102, + rulers = 103, + scrollbar = 104, + scrollBeyondLastColumn = 105, + scrollBeyondLastLine = 106, + scrollPredominantAxis = 107, + selectionClipboard = 108, + selectionHighlight = 109, + selectOnLineNumbers = 110, + showFoldingControls = 111, + showUnused = 112, + snippetSuggestions = 113, + smartSelect = 114, + smoothScrolling = 115, + stickyScroll = 116, + stickyTabStops = 117, + stopRenderingLineAfter = 118, + suggest = 119, + suggestFontSize = 120, + suggestLineHeight = 121, + suggestOnTriggerCharacters = 122, + suggestSelection = 123, + tabCompletion = 124, + tabIndex = 125, + unicodeHighlighting = 126, + unusualLineTerminators = 127, + useShadowDOM = 128, + useTabStops = 129, + wordBreak = 130, + wordSegmenterLocales = 131, + wordSeparators = 132, + wordWrap = 133, + wordWrapBreakAfterCharacters = 134, + wordWrapBreakBeforeCharacters = 135, + wordWrapColumn = 136, + wordWrapOverride1 = 137, + wordWrapOverride2 = 138, + wrappingIndent = 139, + wrappingStrategy = 140, + showDeprecated = 141, + inlayHints = 142, + editorClassName = 143, + pixelRatio = 144, + tabFocusMode = 145, + layoutInfo = 146, + wrappingInfo = 147, + defaultColorDecorators = 148, + colorDecoratorsActivatedOn = 149, + inlineCompletionsAccessibilityVerbose = 150 } /** @@ -366,6 +367,17 @@ export enum GlyphMarginLane { Right = 3 } +export enum HoverVerbosityAction { + /** + * Increase the verbosity of the hover + */ + Increase = 0, + /** + * Decrease the verbosity of the hover + */ + Decrease = 1 +} + /** * Describes what to do with the indentation when pressing Enter. */ @@ -721,6 +733,11 @@ export enum NewSymbolNameTag { AIGenerated = 1 } +export enum NewSymbolNameTriggerKind { + Invoke = 0, + Automatic = 1 +} + /** * A positioning preference for rendering overlay widgets. */ diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 1bcfecfeb97..9afbc5f6075 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -18,19 +18,18 @@ export namespace AccessibilityHelpNLS { export const auto_off = nls.localize("auto_off", "The application is configured to never be optimized for usage with a Screen Reader."); export const screenReaderModeEnabled = nls.localize("screenReaderModeEnabled", "Screen Reader Optimized Mode enabled."); export const screenReaderModeDisabled = nls.localize("screenReaderModeDisabled", "Screen Reader Optimized Mode disabled."); - export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior {0}."); - export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding."); - export const stickScrollKb = nls.localize("stickScrollKb", "Focus Sticky Scroll ({0}) to focus the currently nested scopes."); - export const stickScrollNoKb = nls.localize("stickScrollNoKb", "Focus Sticky Scroll to focus the currently nested scopes. It is currently not triggerable by a keybinding."); - export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior {0}."); - export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding."); + export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior{0}.", ''); + export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior{0}", ''); + export const stickScroll = nls.localize("stickScrollKb", "Focus Sticky Scroll{0} to focus the currently nested scopes.", ''); export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); export const listSignalSounds = nls.localize("listSignalSoundsCommand", "Run the command: List Signal Sounds for an overview of all sounds and their current status."); export const listAlerts = nls.localize("listAnnouncementsCommand", "Run the command: List Signal Announcements for an overview of announcements and their current status."); - export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat ({0}) to open or close a chat session."); - export const quickChatNoKb = nls.localize("quickChatCommandNoKb", "Toggle quick chat is not currently triggerable by a keybinding."); - export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat ({0}) to create an in editor chat session."); - export const startInlineChatNoKb = nls.localize("startInlineChatCommandNoKb", "The command: Start inline chat is not currentlyt riggerable by a keybinding."); + export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat{0} to open or close a chat session.", ''); + export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat{0} to create an in editor chat session.", ''); + export const startDebugging = nls.localize('debug.startDebugging', "The Debug: Start Debugging command{0} will start a debug session.", ''); + export const setBreakpoint = nls.localize('debugConsole.setBreakpoint', "The Debug: Inline Breakpoint command{0} will set or unset a breakpoint at the current cursor position in the active editor.", ''); + export const addToWatch = nls.localize('debugConsole.addToWatch', "The Debug: Add to Watch command{0} will add the selected text to the watch view.", ''); + export const debugExecuteSelection = nls.localize('debugConsole.executeSelection', "The Debug: Execute Selection command{0} will execute the selected text in the debug console.", ''); } export namespace InspectTokensNLS { @@ -57,7 +56,6 @@ export namespace QuickOutlineNLS { export namespace StandaloneCodeEditorNLS { export const editorViewAccessibleLabel = nls.localize('editorViewAccessibleLabel', "Editor content"); - export const accessibilityHelpMessage = nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options."); } export namespace ToggleHighContrastNLS { diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 58c720ac87c..7d63afec8e8 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -55,6 +55,9 @@ export interface IModelContentChange { * An event describing a change in the text of a model. */ export interface IModelContentChangedEvent { + /** + * The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence. + */ readonly changes: IModelContentChange[]; /** * The (new) end-of-line character. diff --git a/src/vs/editor/common/tokenizationRegistry.ts b/src/vs/editor/common/tokenizationRegistry.ts index d9fb1bba82f..15ad1b85159 100644 --- a/src/vs/editor/common/tokenizationRegistry.ts +++ b/src/vs/editor/common/tokenizationRegistry.ts @@ -6,13 +6,13 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent, ILazyTokenizationSupport } from 'vs/editor/common/languages'; +import { ITokenizationRegistry, ITokenizationSupportChangedEvent, ILazyTokenizationSupport } from 'vs/editor/common/languages'; import { ColorId } from 'vs/editor/common/encodedTokenAttributes'; -export class TokenizationRegistry implements ITokenizationRegistry { +export class TokenizationRegistry implements ITokenizationRegistry { - private readonly _tokenizationSupports = new Map(); - private readonly _factories = new Map(); + private readonly _tokenizationSupports = new Map(); + private readonly _factories = new Map>(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; @@ -30,7 +30,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public register(languageId: string, support: ITokenizationSupport): IDisposable { + public register(languageId: string, support: TSupport): IDisposable { this._tokenizationSupports.set(languageId, support); this.handleChange([languageId]); return toDisposable(() => { @@ -42,11 +42,11 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public get(languageId: string): ITokenizationSupport | null { + public get(languageId: string): TSupport | null { return this._tokenizationSupports.get(languageId) || null; } - public registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable { + public registerFactory(languageId: string, factory: ILazyTokenizationSupport): IDisposable { this._factories.get(languageId)?.dispose(); const myData = new TokenizationSupportFactoryData(this, languageId, factory); this._factories.set(languageId, myData); @@ -60,7 +60,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { }); } - public async getOrCreate(languageId: string): Promise { + public async getOrCreate(languageId: string): Promise { // check first if the support is already set const tokenizationSupport = this.get(languageId); if (tokenizationSupport) { @@ -112,7 +112,7 @@ export class TokenizationRegistry implements ITokenizationRegistry { } } -class TokenizationSupportFactoryData extends Disposable { +class TokenizationSupportFactoryData extends Disposable { private _isDisposed: boolean = false; private _resolvePromise: Promise | null = null; @@ -123,9 +123,9 @@ class TokenizationSupportFactoryData extends Disposable { } constructor( - private readonly _registry: TokenizationRegistry, + private readonly _registry: TokenizationRegistry, private readonly _languageId: string, - private readonly _factory: ILazyTokenizationSupport, + private readonly _factory: ILazyTokenizationSupport, ) { super(); } diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index bdcda7cb180..67cdf815001 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -5,10 +5,14 @@ import { ILanguageIdCodec } from 'vs/editor/common/languages'; import { FontStyle, ColorId, StandardTokenType, MetadataConsts, TokenMetadata, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes'; +import { IPosition } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; export interface IViewLineTokens { + languageIdCodec: ILanguageIdCodec; equals(other: IViewLineTokens): boolean; getCount(): number; + getStandardTokenType(tokenIndex: number): StandardTokenType; getForeground(tokenIndex: number): ColorId; getEndOffset(tokenIndex: number): number; getClassName(tokenIndex: number): string; @@ -18,6 +22,8 @@ export interface IViewLineTokens { getLineContent(): string; getMetadata(tokenIndex: number): number; getLanguageId(tokenIndex: number): string; + getTokenText(tokenIndex: number): string; + forEach(callback: (tokenIndex: number) => void): void; } export class LineTokens implements IViewLineTokens { @@ -26,7 +32,8 @@ export class LineTokens implements IViewLineTokens { private readonly _tokens: Uint32Array; private readonly _tokensCount: number; private readonly _text: string; - private readonly _languageIdCodec: ILanguageIdCodec; + + public readonly languageIdCodec: ILanguageIdCodec; public static defaultTokenMetadata = ( (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) @@ -44,11 +51,23 @@ export class LineTokens implements IViewLineTokens { return new LineTokens(tokens, lineContent, decoder); } + public static createFromTextAndMetadata(data: { text: string; metadata: number }[], decoder: ILanguageIdCodec): LineTokens { + let offset: number = 0; + let fullText: string = ''; + const tokens = new Array(); + for (const { text, metadata } of data) { + tokens.push(offset + text.length, metadata); + offset += text.length; + fullText += text; + } + return new LineTokens(new Uint32Array(tokens), fullText, decoder); + } + constructor(tokens: Uint32Array, text: string, decoder: ILanguageIdCodec) { this._tokens = tokens; this._tokensCount = (this._tokens.length >>> 1); this._text = text; - this._languageIdCodec = decoder; + this.languageIdCodec = decoder; } public equals(other: IViewLineTokens): boolean { @@ -98,7 +117,7 @@ export class LineTokens implements IViewLineTokens { public getLanguageId(tokenIndex: number): string { const metadata = this._tokens[(tokenIndex << 1) + 1]; const languageId = TokenMetadata.getLanguageId(metadata); - return this._languageIdCodec.decodeLanguageId(languageId); + return this.languageIdCodec.decodeLanguageId(languageId); } public getStandardTokenType(tokenIndex: number): StandardTokenType { @@ -225,7 +244,21 @@ export class LineTokens implements IViewLineTokens { } } - return new LineTokens(new Uint32Array(newTokens), text, this._languageIdCodec); + return new LineTokens(new Uint32Array(newTokens), text, this.languageIdCodec); + } + + public getTokenText(tokenIndex: number): string { + const startOffset = this.getStartOffset(tokenIndex); + const endOffset = this.getEndOffset(tokenIndex); + const text = this._text.substring(startOffset, endOffset); + return text; + } + + public forEach(callback: (tokenIndex: number) => void): void { + const tokenCount = this.getCount(); + for (let tokenIndex = 0; tokenIndex < tokenCount; tokenIndex++) { + callback(tokenIndex); + } } } @@ -239,12 +272,15 @@ class SliceLineTokens implements IViewLineTokens { private readonly _firstTokenIndex: number; private readonly _tokensCount: number; + public readonly languageIdCodec: ILanguageIdCodec; + constructor(source: LineTokens, startOffset: number, endOffset: number, deltaOffset: number) { this._source = source; this._startOffset = startOffset; this._endOffset = endOffset; this._deltaOffset = deltaOffset; this._firstTokenIndex = source.findTokenIndexAtOffset(startOffset); + this.languageIdCodec = source.languageIdCodec; this._tokensCount = 0; for (let i = this._firstTokenIndex, len = source.getCount(); i < len; i++) { @@ -284,6 +320,10 @@ class SliceLineTokens implements IViewLineTokens { return this._tokensCount; } + public getStandardTokenType(tokenIndex: number): StandardTokenType { + return this._source.getStandardTokenType(this._firstTokenIndex + tokenIndex); + } + public getForeground(tokenIndex: number): ColorId { return this._source.getForeground(this._firstTokenIndex + tokenIndex); } @@ -308,4 +348,36 @@ class SliceLineTokens implements IViewLineTokens { public findTokenIndexAtOffset(offset: number): number { return this._source.findTokenIndexAtOffset(offset + this._startOffset - this._deltaOffset) - this._firstTokenIndex; } + + public getTokenText(tokenIndex: number): string { + const adjustedTokenIndex = this._firstTokenIndex + tokenIndex; + const tokenStartOffset = this._source.getStartOffset(adjustedTokenIndex); + const tokenEndOffset = this._source.getEndOffset(adjustedTokenIndex); + let text = this._source.getTokenText(adjustedTokenIndex); + if (tokenStartOffset < this._startOffset) { + text = text.substring(this._startOffset - tokenStartOffset); + } + if (tokenEndOffset > this._endOffset) { + text = text.substring(0, text.length - (tokenEndOffset - this._endOffset)); + } + return text; + } + + public forEach(callback: (tokenIndex: number) => void): void { + for (let tokenIndex = 0; tokenIndex < this.getCount(); tokenIndex++) { + callback(tokenIndex); + } + } +} + +export function getStandardTokenTypeAtPosition(model: ITextModel, position: IPosition): StandardTokenType | undefined { + const lineNumber = position.lineNumber; + if (!model.tokenization.isCheapToTokenize(lineNumber)) { + return undefined; + } + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); + const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + const tokenType = lineTokens.getStandardTokenType(tokenIndex); + return tokenType; } diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 9356bb2ab01..3ff0d5f0917 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -97,6 +97,8 @@ export interface IViewModel extends ICursorSimpleModel { //#region viewLayout changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): void; //#endregion + + batchEvents(callback: () => void): void; } export interface IViewLayout { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index ea1f1fba62a..50366d54958 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -71,6 +71,7 @@ export class ViewModel extends Disposable implements IViewModel { private readonly languageConfigurationService: ILanguageConfigurationService, private readonly _themeService: IThemeService, private readonly _attachedView: IAttachedView, + private readonly _transactionalTarget: IBatchableTarget, ) { super(); @@ -1102,12 +1103,18 @@ export class ViewModel extends Disposable implements IViewModel { //#endregion private _withViewEventsCollector(callback: (eventsCollector: ViewModelEventsCollector) => T): T { - try { - const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); - return callback(eventsCollector); - } finally { - this._eventDispatcher.endEmitViewEvents(); - } + return this._transactionalTarget.batchChanges(() => { + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + return callback(eventsCollector); + } finally { + this._eventDispatcher.endEmitViewEvents(); + } + }); + } + + public batchEvents(callback: () => void): void { + this._withViewEventsCollector(() => { callback(); }); } normalizePosition(position: Position, affinity: PositionAffinity): Position { @@ -1123,6 +1130,13 @@ export class ViewModel extends Disposable implements IViewModel { } } +export interface IBatchableTarget { + /** + * Allows the target to apply the changes introduced by the callback in a batch. + */ + batchChanges(cb: () => T): T; +} + class ViewportStart implements IDisposable { public static create(model: ITextModel): ViewportStart { diff --git a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts index ffd9e3240dd..b3530eaa888 100644 --- a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts @@ -23,7 +23,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', { dark: '#A0A0A0', light: '#A0A0A0', hcDark: '#A0A0A0', hcLight: '#A0A0A0' }, nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); +const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', '#A0A0A0', nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); class JumpToBracketAction extends EditorAction { constructor() { diff --git a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts index 289fe8aa96a..7a5f7e106e7 100644 --- a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index b18e3dd0b81..e10b43e1f37 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -111,7 +111,6 @@ export const CopyAction = supportsCopy ? registerCommand(new MultiCommand({ MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { submenu: MenuId.MenubarCopy, title: nls.localize2('copy as', "Copy As"), group: '2_ccp', order: 3 }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextCopy, title: nls.localize2('copy as', "Copy As"), group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 3 }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1, when: ContextKeyExpr.and(ContextKeyExpr.notEquals('resourceScheme', 'output'), EditorContextKeys.editorTextFocus) }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { submenu: MenuId.ExplorerContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 }); export const PasteAction = supportsPaste ? registerCommand(new MultiCommand({ diff --git a/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/src/vs/editor/contrib/codeAction/browser/codeAction.ts index c665dd778fe..048d3cb55c9 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -273,7 +273,7 @@ export async function applyCodeAction( codeActionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind (refactor, quickfix) of the applied code action' }; codeActionIsPreferred: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Was the code action marked as being a preferred action?' }; reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of action used to trigger apply code action.' }; - owner: 'mjbvz'; + owner: 'justschen'; comment: 'Event used to gain insights into which code actions are being triggered'; }; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts index 4a11f63060c..1dd22e4c494 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts @@ -45,3 +45,15 @@ Registry.as(Extensions.Configuration).registerConfigurat }, } }); + +Registry.as(Extensions.Configuration).registerConfiguration({ + ...editorConfigurationBaseNode, + properties: { + 'editor.codeActions.triggerOnFocusChange': { + type: 'boolean', + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, + markdownDescription: nls.localize('triggerOnFocusChange', 'Enable triggering {0} when {1} is set to {2}. Code Actions must be set to {3} to be triggered for window and focus changes.', '`#editor.codeActionsOnSave#`', '`#files.autoSave#`', '`afterDelay`', '`always`'), + default: false, + }, + } +}); diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 784be6bebee..5ac2deed516 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -40,7 +40,6 @@ import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; - interface IActionShowOptions { readonly includeDisabledActions?: boolean; readonly fromLightbulb?: boolean; @@ -78,7 +77,7 @@ export class CodeActionController extends Disposable implements IEditorContribut @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -158,11 +157,12 @@ export class CodeActionController extends Disposable implements IEditorContribut public hideLightBulbWidget(): void { this._lightBulbWidget.rawValue?.hide(); + this._lightBulbWidget.rawValue?.gutterHide(); } private async update(newState: CodeActionsState.State): Promise { if (newState.type !== CodeActionsState.Type.Triggered) { - this._lightBulbWidget.rawValue?.hide(); + this.hideLightBulbWidget(); return; } @@ -187,7 +187,7 @@ export class CodeActionController extends Disposable implements IEditorContribut const validActionToApply = this.tryGetValidActionToApply(newState.trigger, actions); if (validActionToApply) { try { - this._lightBulbWidget.value?.hide(); + this.hideLightBulbWidget(); await this._applyCodeAction(validActionToApply, false, false, ApplyCodeActionReason.FromCodeActions); } finally { actions.dispose(); @@ -280,11 +280,11 @@ export class CodeActionController extends Disposable implements IEditorContribut const delegate: IActionListDelegate = { onSelect: async (action: CodeActionItem, preview?: boolean) => { - this._applyCodeAction(action, /* retrigger */ true, !!preview, ApplyCodeActionReason.FromCodeActions); - this._actionWidgetService.hide(); + this._applyCodeAction(action, /* retrigger */ true, !!preview, options.fromLightbulb ? ApplyCodeActionReason.FromAILightbulb : ApplyCodeActionReason.FromCodeActions); + this._actionWidgetService.hide(false); currentDecorations.clear(); }, - onHide: () => { + onHide: (didCancel?) => { this._editor?.focus(); currentDecorations.clear(); }, @@ -301,7 +301,9 @@ export class CodeActionController extends Disposable implements IEditorContribut const refactorKinds = [ CodeActionKind.RefactorExtract, CodeActionKind.RefactorInline, - CodeActionKind.RefactorRewrite + CodeActionKind.RefactorRewrite, + CodeActionKind.RefactorMove, + CodeActionKind.Source ]; canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind)); diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index 9c7e0c73b76..0e11fef86ab 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts @@ -334,6 +334,11 @@ export class CodeActionModel extends Disposable { // Do not trigger state if current state is manual and incoming state is automatic if (!isManualToAutoTransition) { this.setState(newState); + } else { + // Reset the new state after getting code actions back. + setTimeout(() => { + this.setState(newState); + }, 500); } }, undefined); this._codeActionOracle.value.trigger({ type: CodeActionTriggerType.Auto, triggerAction: CodeActionTriggerSource.Default }); diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css index ea05c8574b7..4961d4c92cb 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css @@ -41,6 +41,25 @@ width: 100%; height: 100%; opacity: 0.3; - background-color: var(--vscode-editor-background); z-index: 1; } + +/* gutter decoration */ +.monaco-editor .glyph-margin-widgets .cgmr[class*="codicon-gutter-lightbulb"] { + display: block; + cursor: pointer; +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb, +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-sparkle { + color: var(--vscode-editorLightBulb-foreground); +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-auto-fix, +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-aifix-auto-fix { + color: var(--vscode-editorLightBulbAutoFix-foreground, var(--vscode-editorLightBulb-foreground)); +} + +.monaco-editor .glyph-margin-widgets .cgmr.codicon-gutter-lightbulb-sparkle-filled { + color: var(--vscode-editorLightBulbAi-foreground, var(--vscode-icon-foreground)); +} diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 42564d79f66..6bc8eb0c1b3 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -10,15 +10,24 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./lightBulbWidget'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; +import { GlyphMarginLane, IModelDecorationsChangeAccessor, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { autoFixCommandId, quickFixCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import type { CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; +import { CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Range } from 'vs/editor/common/core/range'; + +const GUTTER_LIGHTBULB_ICON = registerIcon('gutter-lightbulb', Codicon.lightBulb, nls.localize('gutterLightbulbWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor.')); +const GUTTER_LIGHTBULB_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-auto-fix', Codicon.lightbulbAutofix, nls.localize('gutterLightbulbAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and a quick fix is available.')); +const GUTTER_LIGHTBULB_AIFIX_ICON = registerIcon('gutter-lightbulb-sparkle', Codicon.lightbulbSparkle, nls.localize('gutterLightbulbAIFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix is available.')); +const GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-aifix-auto-fix', Codicon.lightbulbSparkleAutofix, nls.localize('gutterLightbulbAIFixAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); +const GUTTER_SPARKLE_FILLED_ICON = registerIcon('gutter-lightbulb-sparkle-filled', Codicon.sparkleFilled, nls.localize('gutterLightbulbSparkleFilledWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); namespace LightBulbState { @@ -44,6 +53,14 @@ namespace LightBulbState { } export class LightBulbWidget extends Disposable implements IContentWidget { + private _gutterDecorationID: string | undefined; + + private static readonly GUTTER_DECORATION = ModelDecorationOptions.register({ + description: 'codicon-gutter-lightbulb-decoration', + glyphMarginClassName: ThemeIcon.asClassName(Codicon.lightBulb), + glyphMargin: { position: GlyphMarginLane.Left }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }); public static readonly ID = 'editor.contrib.lightbulbWidget'; @@ -55,20 +72,22 @@ export class LightBulbWidget extends Disposable implements IContentWidget { public readonly onClick = this._onClick.event; private _state: LightBulbState.State = LightBulbState.Hidden; + private _gutterState: LightBulbState.State = LightBulbState.Hidden; private _iconClasses: string[] = []; private _preferredKbLabel?: string; private _quickFixKbLabel?: string; + private gutterDecoration: ModelDecorationOptions = LightBulbWidget.GUTTER_DECORATION; + constructor( private readonly _editor: ICodeEditor, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ICommandService commandService: ICommandService, + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); this._domNode = dom.$('div.lightBulbWidget'); - + this._domNode.role = 'listbox'; this._register(Gesture.ignoreTarget(this._domNode)); this._editor.addContentWidget(this); @@ -79,6 +98,10 @@ export class LightBulbWidget extends Disposable implements IContentWidget { if (this.state.type !== LightBulbState.Type.Showing || !editorModel || this.state.editorPosition.lineNumber >= editorModel.getLineCount()) { this.hide(); } + + if (this.gutterState.type !== LightBulbState.Type.Showing || !editorModel || this.gutterState.editorPosition.lineNumber >= editorModel.getLineCount()) { + this.gutterHide(); + } })); this._register(dom.addStandardDisposableGenericMouseDownListener(this._domNode, e => { @@ -121,14 +144,54 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._register(Event.runAndSubscribe(this._keybindingService.onDidUpdateKeybindings, () => { this._preferredKbLabel = this._keybindingService.lookupKeybinding(autoFixCommandId)?.getLabel() ?? undefined; this._quickFixKbLabel = this._keybindingService.lookupKeybinding(quickFixCommandId)?.getLabel() ?? undefined; - this._updateLightBulbTitleAndIcon(); })); + + this._register(this._editor.onMouseDown(async (e: IEditorMouseEvent) => { + const lightbulbClasses = [ + 'codicon-' + GUTTER_LIGHTBULB_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id, + 'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id + ]; + + if (!e.target.element || !lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) { + return; + } + + if (this.gutterState.type !== LightBulbState.Type.Showing) { + return; + } + + // Make sure that focus / cursor location is not lost when clicking widget icon + this._editor.focus(); + + // a bit of extra work to make sure the menu + // doesn't cover the line-text + const { top, height } = dom.getDomNodePagePosition(e.target.element); + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + + let pad = Math.floor(lineHeight / 3); + if (this.gutterState.widgetPosition.position !== null && this.gutterState.widgetPosition.position.lineNumber < this.gutterState.editorPosition.lineNumber) { + pad += lineHeight; + } + + this._onClick.fire({ + x: e.event.posx, + y: top + height + pad, + actions: this.gutterState.actions, + trigger: this.gutterState.trigger, + }); + })); } override dispose(): void { super.dispose(); this._editor.removeContentWidget(this); + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + } } getId(): string { @@ -145,17 +208,20 @@ export class LightBulbWidget extends Disposable implements IContentWidget { public update(actions: CodeActionSet, trigger: CodeActionTrigger, atPosition: IPosition) { if (actions.validActions.length <= 0) { + this.gutterHide(); return this.hide(); } const options = this._editor.getOptions(); if (!options.get(EditorOption.lightbulb).enabled) { + this.gutterHide(); return this.hide(); } const model = this._editor.getModel(); if (!model) { + this.gutterHide(); return this.hide(); } @@ -173,8 +239,51 @@ export class LightBulbWidget extends Disposable implements IContentWidget { let effectiveLineNumber = lineNumber; let effectiveColumnNumber = 1; if (!lineHasSpace) { + // Checks if line is empty or starts with any amount of whitespace + const isLineEmptyOrIndented = (lineNumber: number): boolean => { + const lineContent = model.getLineContent(lineNumber); + return /^\s*$|^\s+/.test(lineContent) || lineContent.length <= effectiveColumnNumber; + }; + if (lineNumber > 1 && !isFolded(lineNumber - 1)) { - effectiveLineNumber -= 1; + const lineCount = model.getLineCount(); + const endLine = lineNumber === lineCount; + const prevLineEmptyOrIndented = lineNumber > 1 && isLineEmptyOrIndented(lineNumber - 1); + const nextLineEmptyOrIndented = !endLine && isLineEmptyOrIndented(lineNumber + 1); + const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber); + const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented; + + let hasDecoration = false; + const currLineDecorations = this._editor.getLineDecorations(lineNumber); + if (currLineDecorations) { + for (const decoration of currLineDecorations) { + if (decoration.options.glyphMarginClassName?.includes(Codicon.debugBreakpoint.id)) { + hasDecoration = true; + } + } + } + + // check above and below. if both are blocked, display lightbulb in the gutter. + if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) { + this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, + preference: LightBulbWidget._posPref + }); + this.renderGutterLightbub(); + return this.hide(); + } else if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { + effectiveLineNumber -= 1; + } else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) { + effectiveLineNumber += 1; + } + } else if (lineNumber === 1 && (lineNumber === model.getLineCount() || !isLineEmptyOrIndented(lineNumber + 1) && !isLineEmptyOrIndented(lineNumber))) { + // special checks for first line blocked vs. not blocked. + this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, + preference: LightBulbWidget._posPref + }); + this.renderGutterLightbub(); + return this.hide(); } else if ((lineNumber < model.getLineCount()) && !isFolded(lineNumber + 1)) { effectiveLineNumber += 1; } else if (column * fontInfo.spaceWidth < 22) { @@ -189,6 +298,19 @@ export class LightBulbWidget extends Disposable implements IContentWidget { position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, preference: LightBulbWidget._posPref }); + + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + this.gutterHide(); + } + + const validActions = actions.validActions; + const actionKind = actions.validActions[0].action.kind; + if (validActions.length !== 1 || !actionKind) { + this._editor.layoutContentWidget(this); + return; + } + this._editor.layoutContentWidget(this); } @@ -201,6 +323,18 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._editor.layoutContentWidget(this); } + public gutterHide(): void { + if (this.gutterState === LightBulbState.Hidden) { + return; + } + + if (this._gutterDecorationID) { + this._removeGutterDecoration(this._gutterDecorationID); + } + + this.gutterState = LightBulbState.Hidden; + } + private get state(): LightBulbState.State { return this._state; } private set state(value) { @@ -208,6 +342,13 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._updateLightBulbTitleAndIcon(); } + private get gutterState(): LightBulbState.State { return this._gutterState; } + + private set gutterState(value) { + this._gutterState = value; + this._updateGutterLightBulbTitleAndIcon(); + } + private _updateLightBulbTitleAndIcon(): void { this._domNode.classList.remove(...this._iconClasses); this._iconClasses = []; @@ -237,6 +378,74 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._domNode.classList.add(...this._iconClasses); } + private _updateGutterLightBulbTitleAndIcon(): void { + if (this.gutterState.type !== LightBulbState.Type.Showing) { + return; + } + let icon: ThemeIcon; + let autoRun = false; + if (this.gutterState.actions.allAIFixes) { + icon = GUTTER_SPARKLE_FILLED_ICON; + if (this.gutterState.actions.validActions.length === 1) { + autoRun = true; + } + } else if (this.gutterState.actions.hasAutoFix) { + if (this.gutterState.actions.hasAIFix) { + icon = GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON; + } else { + icon = GUTTER_LIGHTBULB_AUTO_FIX_ICON; + } + } else if (this.gutterState.actions.hasAIFix) { + icon = GUTTER_LIGHTBULB_AIFIX_ICON; + } else { + icon = GUTTER_LIGHTBULB_ICON; + } + this._updateLightbulbTitle(this.gutterState.actions.hasAutoFix, autoRun); + + const GUTTER_DECORATION = ModelDecorationOptions.register({ + description: 'codicon-gutter-lightbulb-decoration', + glyphMarginClassName: ThemeIcon.asClassName(icon), + glyphMargin: { position: GlyphMarginLane.Left }, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }); + + this.gutterDecoration = GUTTER_DECORATION; + } + + /* Gutter Helper Functions */ + private renderGutterLightbub(): void { + const selection = this._editor.getSelection(); + if (!selection) { + return; + } + + if (this._gutterDecorationID === undefined) { + this._addGutterDecoration(selection.startLineNumber); + } else { + this._updateGutterDecoration(this._gutterDecorationID, selection.startLineNumber); + } + } + + private _addGutterDecoration(lineNumber: number) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + this._gutterDecorationID = accessor.addDecoration(new Range(lineNumber, 0, lineNumber, 0), this.gutterDecoration); + }); + } + + private _removeGutterDecoration(decorationId: string) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + accessor.removeDecoration(decorationId); + this._gutterDecorationID = undefined; + }); + } + + private _updateGutterDecoration(decorationId: string, lineNumber: number) { + this._editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => { + accessor.changeDecoration(decorationId, new Range(lineNumber, 0, lineNumber, 0)); + accessor.changeDecorationOptions(decorationId, this.gutterDecoration); + }); + } + private _updateLightbulbTitle(autoFix: boolean, autoRun: boolean): void { if (this.state.type !== LightBulbState.Type.Showing) { return; diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts index c1783a39f11..a98da2b0e0d 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts index 641f1491d6f..664a36b2dca 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCodeChord } from 'vs/base/common/keybindings'; import { KeyCode } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; @@ -101,4 +101,3 @@ function createCodeActionKeybinding(keycode: KeyCode, command: string, commandAr null, false); } - diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts index 71d6df33c0b..5946fb24f50 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; diff --git a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts index 67fa3b8009c..f7666b8c246 100644 --- a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts +++ b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts @@ -61,18 +61,17 @@ export class CodeLensCache implements ICodeLensCache { this._deserialize(raw); // store lens data on shutdown - Event.once(storageService.onWillSaveState)(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - storageService.store(key, this._serialize(), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } + const onWillSaveStateBecauseOfShutdown = Event.filter(storageService.onWillSaveState, e => e.reason === WillSaveStateReason.SHUTDOWN); + Event.once(onWillSaveStateBecauseOfShutdown)(e => { + storageService.store(key, this._serialize(), StorageScope.WORKSPACE, StorageTarget.MACHINE); }); } put(model: ITextModel, data: CodeLensModel): void { // create a copy of the model that is without command-ids // but with comand-labels - const copyItems = data.lenses.map(item => { - return { + const copyItems = data.lenses.map((item): CodeLens => { + return { range: item.symbol.range, command: item.symbol.command && { id: '', title: item.symbol.command?.title }, }; diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index 2cfcb40b456..494336475f3 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -229,7 +229,7 @@ export class CodeLensContribution implements IEditorContribution { this._resolveCodeLensesPromise?.cancel(); this._resolveCodeLensesPromise = undefined; })); - this._localToDispose.add(this._editor.onDidFocusEditorWidget(() => { + this._localToDispose.add(this._editor.onDidFocusEditorText(() => { scheduler.schedule(); })); this._localToDispose.add(this._editor.onDidBlurEditorText(() => { diff --git a/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts b/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts index d6ff1d4d230..e7642a17e7d 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorContributions.ts @@ -11,7 +11,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ColorDecorationInjectedTextMarker } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ColorHoverParticipant } from 'vs/editor/contrib/colorPicker/browser/colorHoverParticipant'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hover'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index 23ff1742e84..150f23daa07 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -195,7 +195,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { }); } - private _colorDecorationClassRefs = this._register(new DisposableStore()); + private readonly _colorDecorationClassRefs = this._register(new DisposableStore()); private updateColorDecorators(colorData: IColorData[]): void { this._colorDecorationClassRefs.clear(); diff --git a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts index e37a442f643..32956ec08da 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts @@ -6,7 +6,7 @@ import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; @@ -16,11 +16,12 @@ import { getColorPresentations, getColors } from 'vs/editor/contrib/colorPicker/ import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/browser/colorPickerModel'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { Dimension } from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; export class ColorHover implements IHoverPart { @@ -50,6 +51,8 @@ export class ColorHoverParticipant implements IEditorHoverParticipant { + const renderedPart = renderHoverParts(this, this._editor, this._themeService, hoverParts, context); + if (!renderedPart) { + return new RenderedHoverParts([]); + } + this._colorPicker = renderedPart.colorPicker; + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: renderedPart.hoverPart, + hoverElement: this._colorPicker.domNode, + dispose() { renderedPart.disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } + + public getAccessibleContent(hoverPart: ColorHover): string { + return nls.localize('hoverAccessibilityColorParticipant', 'There is a color picker here.'); + } + + public handleResize(): void { + this._colorPicker?.layout(); + } + + public isColorPickerVisible(): boolean { + return !!this._colorPicker; } } @@ -146,7 +171,7 @@ export class StandaloneColorPickerParticipant { } } - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ColorHover[] | StandaloneColorPickerHover[]): IDisposable { + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: StandaloneColorPickerHover[]): { disposables: IDisposable; hoverPart: StandaloneColorPickerHover; colorPicker: ColorPickerWidget } | undefined { return renderHoverParts(this, this._editor, this._themeService, hoverParts, context); } @@ -178,9 +203,9 @@ async function _createColorHover(participant: ColorHoverParticipant | Standalone } } -function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPickerParticipant, editor: ICodeEditor, themeService: IThemeService, hoverParts: ColorHover[] | StandaloneColorPickerHover[], context: IEditorHoverRenderContext) { +function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPickerParticipant, editor: ICodeEditor, themeService: IThemeService, hoverParts: T[], context: IEditorHoverRenderContext): { hoverPart: T; colorPicker: ColorPickerWidget; disposables: DisposableStore } | undefined { if (hoverParts.length === 0 || !editor.hasModel()) { - return Disposable.None; + return undefined; } if (context.setMinimumDimensions) { const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8; @@ -191,13 +216,12 @@ function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPi const colorHover = hoverParts[0]; const editorModel = editor.getModel(); const model = colorHover.model; - const widget = disposables.add(new ColorPickerWidget(context.fragment, model, editor.getOption(EditorOption.pixelRatio), themeService, participant instanceof StandaloneColorPickerParticipant)); - context.setColorPicker(widget); + const colorPicker = disposables.add(new ColorPickerWidget(context.fragment, model, editor.getOption(EditorOption.pixelRatio), themeService, participant instanceof StandaloneColorPickerParticipant)); let editorUpdatedByColorPicker = false; let range = new Range(colorHover.range.startLineNumber, colorHover.range.startColumn, colorHover.range.endLineNumber, colorHover.range.endColumn); if (participant instanceof StandaloneColorPickerParticipant) { - const color = hoverParts[0].model.color; + const color = colorHover.model.color; participant.color = color; _updateColorPresentations(editorModel, model, color, range, colorHover); disposables.add(model.onColorFlushed((color: Color) => { @@ -221,7 +245,7 @@ function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPi editor.focus(); } })); - return disposables; + return { hoverPart: colorHover, colorPicker, disposables }; } function _updateEditorModel(editor: IActiveCodeEditor, range: Range, model: ColorPickerModel): Range { diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts index 7688defca5f..b911fe5b0f5 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts @@ -468,6 +468,7 @@ export class InsertButton extends Disposable { export class ColorPickerWidget extends Widget implements IEditorHoverColorPickerWidget { private static readonly ID = 'editor.contrib.colorPickerWidget'; + private readonly _domNode: HTMLElement; body: ColorPickerBody; header: ColorPickerHeader; @@ -477,11 +478,11 @@ export class ColorPickerWidget extends Widget implements IEditorHoverColorPicker this._register(PixelRatio.getInstance(dom.getWindow(container)).onDidChange(() => this.layout())); - const element = $('.colorpicker-widget'); - container.appendChild(element); + this._domNode = $('.colorpicker-widget'); + container.appendChild(this._domNode); - this.header = this._register(new ColorPickerHeader(element, this.model, themeService, standaloneColorPicker)); - this.body = this._register(new ColorPickerBody(element, this.model, this.pixelRatio, standaloneColorPicker)); + this.header = this._register(new ColorPickerHeader(this._domNode, this.model, themeService, standaloneColorPicker)); + this.body = this._register(new ColorPickerBody(this._domNode, this.model, this.pixelRatio, standaloneColorPicker)); } getId(): string { @@ -491,4 +492,8 @@ export class ColorPickerWidget extends Widget implements IEditorHoverColorPicker layout(): void { this.body.layout(); } + + get domNode(): HTMLElement { + return this._domNode; + } } diff --git a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerActions.ts b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerActions.ts index a399e560ed4..9d531552008 100644 --- a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerActions.ts +++ b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerActions.ts @@ -24,7 +24,10 @@ export class ShowOrFocusStandaloneColorPicker extends EditorAction2 { precondition: undefined, menu: [ { id: MenuId.CommandPalette }, - ] + ], + metadata: { + description: localize2('showOrFocusStandaloneColorPickerDescription', "Show or focus a standalone color picker which uses the default color provider. It displays hex/rgb/hsl colors."), + } }); } runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { @@ -47,6 +50,9 @@ class HideStandaloneColorPicker extends EditorAction { kbOpts: { primary: KeyCode.Escape, weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: localize2('hideColorPickerDescription', "Hide the standalone color picker."), } }); } @@ -70,6 +76,9 @@ class InsertColorWithStandaloneColorPicker extends EditorAction { kbOpts: { primary: KeyCode.Enter, weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: localize2('insertColorWithStandaloneColorPickerDescription', "Insert hex/rgb/hsl colors with the focused standalone color picker."), } }); } diff --git a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts index a6ef1b75116..424448611b1 100644 --- a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts @@ -10,9 +10,9 @@ import { PositionAffinity } from 'vs/editor/common/model'; import { Position } from 'vs/editor/common/core/position'; import { StandaloneColorPickerHover, StandaloneColorPickerParticipant } from 'vs/editor/contrib/colorPicker/browser/colorHoverParticipant'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHover'; +import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ColorPickerWidget, InsertButton } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; +import { InsertButton } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; import { Emitter } from 'vs/base/common/event'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IColorInformation } from 'vs/editor/common/languages'; @@ -215,42 +215,42 @@ export class StandaloneColorPickerWidget extends Disposable implements IContentW private _render(colorHover: StandaloneColorPickerHover, foundInEditor: boolean) { const fragment = document.createDocumentFragment(); const statusBar = this._register(new EditorHoverStatusBar(this._keybindingService)); - let colorPickerWidget: ColorPickerWidget | undefined; const context: IEditorHoverRenderContext = { fragment, statusBar, - setColorPicker: (widget: ColorPickerWidget) => colorPickerWidget = widget, onContentsChanged: () => { }, hide: () => this.hide() }; this._colorHover = colorHover; - this._register(this._standaloneColorPickerParticipant.renderHoverParts(context, [colorHover])); - if (colorPickerWidget === undefined) { + const renderedHoverPart = this._standaloneColorPickerParticipant.renderHoverParts(context, [colorHover]); + if (!renderedHoverPart) { return; } + this._register(renderedHoverPart.disposables); + const colorPicker = renderedHoverPart.colorPicker; this._body.classList.add('standalone-colorpicker-body'); this._body.style.maxHeight = Math.max(this._editor.getLayoutInfo().height / 4, 250) + 'px'; this._body.style.maxWidth = Math.max(this._editor.getLayoutInfo().width * 0.66, 500) + 'px'; this._body.tabIndex = 0; this._body.appendChild(fragment); - colorPickerWidget.layout(); + colorPicker.layout(); - const colorPickerBody = colorPickerWidget.body; + const colorPickerBody = colorPicker.body; const saturationBoxWidth = colorPickerBody.saturationBox.domNode.clientWidth; const widthOfOriginalColorBox = colorPickerBody.domNode.clientWidth - saturationBoxWidth - CLOSE_BUTTON_WIDTH - PADDING; - const enterButton: InsertButton | null = colorPickerWidget.body.enterButton; + const enterButton: InsertButton | null = colorPicker.body.enterButton; enterButton?.onClicked(() => { this.updateEditor(); this.hide(); }); - const colorPickerHeader = colorPickerWidget.header; + const colorPickerHeader = colorPicker.header; const pickedColorNode = colorPickerHeader.pickedColorNode; pickedColorNode.style.width = saturationBoxWidth + PADDING + 'px'; const originalColorNode = colorPickerHeader.originalColorNode; originalColorNode.style.width = widthOfOriginalColorBox + 'px'; - const closeButton = colorPickerWidget.header.closeButton; + const closeButton = colorPicker.header.closeButton; closeButton?.onClicked(() => { this.hide(); }); diff --git a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts index ff394a835bb..f40f7b1e252 100644 --- a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index f6cbfc425d7..650a15e2e36 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -148,7 +148,7 @@ export class ContextMenuController implements IEditorContribution { // Find actions available for menu const menuActions = this._getMenuActions(this._editor.getModel(), - this._editor.isSimpleWidget ? MenuId.SimpleEditorContext : MenuId.EditorContext); + this._editor.contextMenuId); // Show menu if we have actions to show if (menuActions.length > 0) { @@ -160,9 +160,7 @@ export class ContextMenuController implements IEditorContribution { const result: IAction[] = []; // get menu groups - const menu = this._menuService.createMenu(menuId, this._contextKeyService); - const groups = menu.getActions({ arg: model.uri }); - menu.dispose(); + const groups = this._menuService.getMenuActions(menuId, this._contextKeyService, { arg: model.uri }); // translate them into other actions for (const group of groups) { @@ -227,7 +225,7 @@ export class ContextMenuController implements IEditorContribution { // Show menu this._contextMenuIsBeingShownCount++; this._contextMenuService.showContextMenu({ - domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined, + domForShadowRoot: useShadowDOM ? this._editor.getOverflowWidgetsDomNode() ?? this._editor.getDomNode() : undefined, getAnchor: () => anchor, diff --git a/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts b/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts index 3cecf3ee8eb..d0e3423a47b 100644 --- a/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts +++ b/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts b/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts index 194b8ee4f16..185d30db3e6 100644 --- a/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts +++ b/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 87cddb00a8d..3406894faa7 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -5,11 +5,11 @@ import { addDisposableListener, getActiveDocument } from 'vs/base/browser/dom'; import { coalesce } from 'vs/base/common/arrays'; -import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { UriList, VSDataTransfer, createStringDataTransferItem, matchesMimeType } from 'vs/base/common/dataTransfer'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import * as platform from 'vs/base/common/platform'; import { generateUuid } from 'vs/base/common/uuid'; @@ -36,6 +36,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { PostEditWidgetManager } from './postEditWidget'; +import { CancellationError, isCancellationError } from 'vs/base/common/errors'; export const changePasteTypeCommandId = 'editor.changePasteType'; @@ -54,6 +55,12 @@ type PasteEditWithProvider = DocumentPasteEdit & { provider: DocumentPasteEditProvider; }; + +interface DocumentPasteWithProviderEditsSession { + edits: readonly PasteEditWithProvider[]; + dispose(): void; +} + type PastePreference = | HierarchicalKind | { providerId: string }; @@ -141,12 +148,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - if (platform.isWeb) { - // Explicitly clear the web resources clipboard. - // This is needed because on web, the browser clipboard is faked out using an in-memory store. - // This means the resources clipboard is not properly updated when copying from the editor. - this._clipboardService.writeResources([]); - } + // Explicitly clear the clipboard internal state. + // This is needed because on web, the browser clipboard is faked out using an in-memory store. + // This means the resources clipboard is not properly updated when copying from the editor. + this._clipboardService.clearInternalState?.(); if (!e.clipboardData || !this.isPasteAsEnabled()) { return; @@ -299,17 +304,28 @@ export class CopyPasteController extends Disposable implements IEditorContributi } private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void { - const p = createCancelablePromise(async (token) => { + const editor = this._editor; + if (!editor.hasModel()) { + return; + } + + const editorStateCts = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined); + + const p = createCancelablePromise(async (pToken) => { const editor = this._editor; if (!editor.hasModel()) { return; } const model = editor.getModel(); - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token); + const disposables = new DisposableStore(); + const cts = disposables.add(new CancellationTokenSource(pToken)); + disposables.add(editorStateCts.token.onCancellationRequested(() => cts.cancel())); + + const token = cts.token; try { - await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); - if (tokenSource.token.isCancellationRequested) { + await this.mergeInDataFromCopy(dataTransfer, metadata, token); + if (token.isCancellationRequested) { return; } @@ -317,43 +333,75 @@ export class CopyPasteController extends Disposable implements IEditorContributi if (!supportedProviders.length || (supportedProviders.length === 1 && supportedProviders[0] instanceof DefaultTextPasteOrDropEditProvider) // Only our default text provider is active ) { - return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } const context: DocumentPasteContext = { triggerKind: DocumentPasteTriggerKind.Automatic, }; - const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); - if (tokenSource.token.isCancellationRequested) { + const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token); + disposables.add(editSession); + if (token.isCancellationRequested) { return; } // If the only edit returned is our default text edit, use the default paste handler - if (providerEdits.length === 1 && providerEdits[0].provider instanceof DefaultTextPasteOrDropEditProvider) { - return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + if (editSession.edits.length === 1 && editSession.edits[0].provider instanceof DefaultTextPasteOrDropEditProvider) { + return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } - if (providerEdits.length) { + if (editSession.edits.length) { const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste'; - return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: providerEdits }, canShowWidget, async (edit, token) => { - const resolved = await edit.provider.resolveDocumentPasteEdit?.(edit, token); - if (resolved) { - edit.additionalEdit = resolved.additionalEdit; - } - return edit; - }, tokenSource.token); + return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: editSession.edits }, canShowWidget, (edit, token) => { + return new Promise((resolve, reject) => { + (async () => { + try { + const resolveP = edit.provider.resolveDocumentPasteEdit?.(edit, token); + const showP = new DeferredPromise(); + const resolved = resolveP && await this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('resolveProcess', "Resolving paste edit. Click to cancel"), Promise.race([showP.p, resolveP]), { + cancel: () => { + showP.cancel(); + return reject(new CancellationError()); + } + }, 0); + if (resolved) { + edit.additionalEdit = resolved.additionalEdit; + } + return resolve(edit); + } catch (err) { + return reject(err); + } + })(); + }); + }, token); } - await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + await this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentPasteOperation === p) { this._currentPasteOperation = undefined; } } }); - this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel"), p); + this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel and do basic paste"), p, { + cancel: async () => { + try { + p.cancel(); + + if (editorStateCts.token.isCancellationRequested) { + return; + } + + await this.applyDefaultPasteHandler(dataTransfer, metadata, editorStateCts.token, clipboardEvent); + } finally { + editorStateCts.dispose(); + } + } + }).then(() => { + editorStateCts.dispose(); + }); this._currentPasteOperation = p; } @@ -365,7 +413,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi } const model = editor.getModel(); - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token); + const disposables = new DisposableStore(); + const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token)); try { await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); if (tokenSource.token.isCancellationRequested) { @@ -383,23 +432,26 @@ export class CopyPasteController extends Disposable implements IEditorContributi triggerKind: DocumentPasteTriggerKind.PasteAs, only: preference && preference instanceof HierarchicalKind ? preference : undefined, }; - let providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); + let editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token)); if (tokenSource.token.isCancellationRequested) { return; } // Filter out any edits that don't match the requested kind if (preference) { - providerEdits = providerEdits.filter(edit => { - if (preference instanceof HierarchicalKind) { - return preference.contains(edit.kind); - } else { - return preference.providerId === edit.provider.id; - } - }); + editSession = { + edits: editSession.edits.filter(edit => { + if (preference instanceof HierarchicalKind) { + return preference.contains(edit.kind); + } else { + return preference.providerId === edit.provider.id; + } + }), + dispose: editSession.dispose + }; } - if (!providerEdits.length) { + if (!editSession.edits.length) { if (context.only) { this.showPasteAsNoEditMessage(selections, context.only); } @@ -408,10 +460,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi let pickedEdit: DocumentPasteEdit | undefined; if (preference) { - pickedEdit = providerEdits.at(0); + pickedEdit = editSession.edits.at(0); } else { const selected = await this._quickInputService.pick( - providerEdits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ + editSession.edits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ label: edit.title, description: edit.kind?.value, edit, @@ -428,7 +480,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, selections, pickedEdit); await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor }); } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentPasteOperation === p) { this._currentPasteOperation = undefined; } @@ -499,23 +551,32 @@ export class CopyPasteController extends Disposable implements IEditorContributi } } - private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { + private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { + const disposables = new DisposableStore(); + const results = await raceCancellation( Promise.all(providers.map(async provider => { try { const edits = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token); - // TODO: dispose of edits + if (edits) { + disposables.add(edits); + } return edits?.edits?.map(edit => ({ ...edit, provider })); } catch (err) { - console.error(err); + if (!isCancellationError(err)) { + console.error(err); + } + return undefined; } - return undefined; })), token); const edits = coalesce(results ?? []).flat().filter(edit => { return !context.only || context.only.contains(edit.kind); }); - return sortEditsByYieldTo(edits); + return { + edits: sortEditsByYieldTo(edits), + dispose: () => disposables.dispose() + }; } private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent) { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 88ffc64d4bd..e44d9341c9b 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -14,14 +14,14 @@ import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; -import { DocumentOnDropEdit, DocumentOnDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; +import { DocumentDropEditProvider, DocumentDropEditsSession, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, DocumentPasteEditProvider { +abstract class SimplePasteAndDropProvider implements DocumentDropEditProvider, DocumentPasteEditProvider { abstract readonly kind: HierarchicalKind; abstract readonly dropMimeTypes: readonly string[] | undefined; @@ -34,14 +34,20 @@ abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider, } return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], dispose() { }, - edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] }; } - async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] : undefined; + if (!edit) { + return; + } + return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], + dispose() { }, + }; } protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise; @@ -219,9 +225,9 @@ export class DefaultDropProvidersFeature extends Disposable { ) { super(); - this._register(languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultTextPasteOrDropEditProvider())); - this._register(languageFeaturesService.documentOnDropEditProvider.register('*', new PathProvider())); - this._register(languageFeaturesService.documentOnDropEditProvider.register('*', new RelativePathProvider(workspaceContextService))); + this._register(languageFeaturesService.documentDropEditProvider.register('*', new DefaultTextPasteOrDropEditProvider())); + this._register(languageFeaturesService.documentDropEditProvider.register('*', new PathProvider())); + this._register(languageFeaturesService.documentDropEditProvider.register('*', new RelativePathProvider(workspaceContextService))); } } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 32e64cff650..082b7447881 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -7,14 +7,14 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { VSDataTransfer, matchesMimeType } from 'vs/base/common/dataTransfer'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { DocumentOnDropEdit, DocumentOnDropEditProvider } from 'vs/editor/common/languages'; +import { DocumentDropEdit, DocumentDropEditProvider } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; @@ -46,7 +46,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr private _currentOperation?: CancelablePromise; private readonly _dropProgressManager: InlineProgressManager; - private readonly _postDropWidgetManager: PostEditWidgetManager; + private readonly _postDropWidgetManager: PostEditWidgetManager; private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance(); @@ -84,8 +84,9 @@ export class DropIntoEditorController extends Disposable implements IEditorContr editor.setPosition(position); const p = createCancelablePromise(async (token) => { - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token); + const disposables = new DisposableStore(); + const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token)); try { const ourDataTransfer = await this.extractDataTransferData(dragEvent); if (ourDataTransfer.size === 0 || tokenSource.token.isCancellationRequested) { @@ -97,7 +98,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return; } - const providers = this._languageFeaturesService.documentOnDropEditProvider + const providers = this._languageFeaturesService.documentDropEditProvider .ordered(model) .filter(provider => { if (!provider.dropMimeTypes) { @@ -107,34 +108,39 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime)); }); - const edits = await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource); + const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource)); if (tokenSource.token.isCancellationRequested) { return; } - if (edits.length) { - const activeEditIndex = this.getInitialActiveEditIndex(model, edits); + if (editSession.edits.length) { + const activeEditIndex = this.getInitialActiveEditIndex(model, editSession.edits); const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop'; // Pass in the parent token here as it tracks cancelling the entire drop operation - await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, async edit => edit, token); + await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: editSession.edits }, canShowWidget, async edit => edit, token); } } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentOperation === p) { this._currentOperation = undefined; } } }); - this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p); + this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p, { cancel: () => p.cancel() }); this._currentOperation = p; } - private async getDropEdits(providers: readonly DocumentOnDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { + private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { + const disposables = new DisposableStore(); + const results = await raceCancellation(Promise.all(providers.map(async provider => { try { - const edits = await provider.provideDocumentOnDropEdits(model, position, dataTransfer, tokenSource.token); - return edits?.map(edit => ({ ...edit, providerId: provider.id })); + const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, tokenSource.token); + if (edits) { + disposables.add(edits); + } + return edits?.edits.map(edit => ({ ...edit, providerId: provider.id })); } catch (err) { console.error(err); } @@ -142,10 +148,13 @@ export class DropIntoEditorController extends Disposable implements IEditorContr })), tokenSource.token); const edits = coalesce(results ?? []).flat(); - return sortEditsByYieldTo(edits); + return { + edits: sortEditsByYieldTo(edits), + dispose: () => disposables.dispose() + }; } - private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { + private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { const preferredProviders = this._configService.getValue>(defaultProviderConfig, { resource: model.uri }); for (const [configMime, desiredKindStr] of Object.entries(preferredProviders)) { const desiredKind = new HierarchicalKind(desiredKindStr); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts index 81cc89436cf..e612cfc0bfc 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/edit.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { DocumentOnDropEdit, DocumentPasteEdit, DropYieldTo, WorkspaceEdit } from 'vs/editor/common/languages'; +import { DocumentDropEdit, DocumentPasteEdit, DropYieldTo, WorkspaceEdit } from 'vs/editor/common/languages'; import { Range } from 'vs/editor/common/core/range'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; @@ -14,7 +14,7 @@ import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; * Given a {@link DropOrPasteEdit} and set of ranges, creates a {@link WorkspaceEdit} that applies the insert text from * the {@link DropOrPasteEdit} at each range plus any additional edits. */ -export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], edit: DocumentPasteEdit | DocumentOnDropEdit): WorkspaceEdit { +export function createCombinedWorkspaceEdit(uri: URI, ranges: readonly Range[], edit: DocumentPasteEdit | DocumentDropEdit): WorkspaceEdit { // If the edit insert text is empty, skip applying at each range if (typeof edit.insertText === 'string' ? edit.insertText === '' : edit.insertText.snippet === '') { return { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts index 357a481eb87..9443f52cc05 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts @@ -7,22 +7,26 @@ import * as dom from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { toAction } from 'vs/base/common/actions'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { isCancellationError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./postEditWidget'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; -import { DocumentOnDropEdit, DocumentPasteEdit } from 'vs/editor/common/languages'; +import { DocumentDropEdit, DocumentPasteEdit } from 'vs/editor/common/languages'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { createCombinedWorkspaceEdit } from 'vs/editor/contrib/dropOrPasteInto/browser/edit'; +import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; -interface EditSet { +interface EditSet { readonly activeEditIndex: number; readonly allEdits: ReadonlyArray; } @@ -32,7 +36,7 @@ interface ShowCommand { readonly label: string; } -class PostEditWidget extends Disposable implements IContentWidget { +class PostEditWidget extends Disposable implements IContentWidget { private static readonly baseId = 'editor.widget.postEditWidget'; readonly allowEditorOverflow = true; @@ -132,7 +136,7 @@ class PostEditWidget extends D } } -export class PostEditWidgetManager extends Disposable { +export class PostEditWidgetManager extends Disposable { private readonly _currentWidget = this._register(new MutableDisposable>()); @@ -143,6 +147,7 @@ export class PostEditWidgetManager { + const model = this._editor.getModel(); + if (!model) { + return; + } + + await model.undo(); + this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token); + }; + + const handleError = (e: Error, message: string) => { + if (isCancellationError(e)) { + return; + } + + this._notificationService.error(message); + if (canShowWidget) { + this.show(ranges[0], edits, onDidSelectEdit); + } + }; + + let resolvedEdit: T; + try { + resolvedEdit = await resolve(edit, token); + } catch (e) { + return handleError(e, localize('resolveError', "Error resolving edit '{0}':\n{1}", edit.title, toErrorMessage(e))); + } + + if (token.isCancellationRequested) { + return; + } const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, resolvedEdit); @@ -174,25 +209,24 @@ export class PostEditWidgetManager 1) { - this.show(editRange ?? primaryRange, edits, async (newEditIndex) => { - const model = this._editor.getModel(); - if (!model) { - return; - } + if (token.isCancellationRequested) { + return; + } - await model.undo(); - this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token); - }); + if (canShowWidget && editResult.isApplied && edits.allEdits.length > 1) { + this.show(editRange ?? primaryRange, edits, onDidSelectEdit); } } diff --git a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts index 3013bcde756..ec61f04a463 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts @@ -2,14 +2,14 @@ * 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 assert from 'assert'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { DocumentOnDropEdit } from 'vs/editor/common/languages'; +import { DocumentDropEdit } from 'vs/editor/common/languages'; import { sortEditsByYieldTo } from 'vs/editor/contrib/dropOrPasteInto/browser/edit'; -function createTestEdit(kind: string, args?: Partial): DocumentOnDropEdit { +function createTestEdit(kind: string, args?: Partial): DocumentDropEdit { return { title: '', insertText: '', @@ -21,13 +21,13 @@ function createTestEdit(kind: string, args?: Partial): Docum suite('sortEditsByYieldTo', () => { test('Should noop for empty edits', () => { - const edits: DocumentOnDropEdit[] = []; + const edits: DocumentDropEdit[] = []; assert.deepStrictEqual(sortEditsByYieldTo(edits), []); }); test('Yielded to edit should get sorted after target', () => { - const edits: DocumentOnDropEdit[] = [ + const edits: DocumentDropEdit[] = [ createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('b') }] }), createTestEdit('b'), ]; @@ -36,7 +36,7 @@ suite('sortEditsByYieldTo', () => { test('Should handle chain of yield to', () => { { - const edits: DocumentOnDropEdit[] = [ + const edits: DocumentDropEdit[] = [ createTestEdit('c', { yieldTo: [{ kind: new HierarchicalKind('a') }] }), createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('b') }] }), createTestEdit('b'), @@ -45,7 +45,7 @@ suite('sortEditsByYieldTo', () => { assert.deepStrictEqual(sortEditsByYieldTo(edits).map(x => x.kind?.value), ['b', 'a', 'c']); } { - const edits: DocumentOnDropEdit[] = [ + const edits: DocumentDropEdit[] = [ createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('b') }] }), createTestEdit('c', { yieldTo: [{ kind: new HierarchicalKind('a') }] }), createTestEdit('b'), @@ -56,7 +56,7 @@ suite('sortEditsByYieldTo', () => { }); test(`Should not reorder when yield to isn't used`, () => { - const edits: DocumentOnDropEdit[] = [ + const edits: DocumentDropEdit[] = [ createTestEdit('c', { yieldTo: [{ kind: new HierarchicalKind('x') }] }), createTestEdit('a', { yieldTo: [{ kind: new HierarchicalKind('y') }] }), createTestEdit('b'), diff --git a/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts b/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts index cc2a8d0f93e..239da13d225 100644 --- a/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts +++ b/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index 5dd9bcc6fb5..bbcc79d0750 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -31,6 +31,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, themeColorFromId } from 'vs/platform/theme/common/themeService'; import { Selection } from 'vs/editor/common/core/selection'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -99,6 +100,7 @@ export class CommonFindController extends Disposable implements IEditorContribut private readonly _clipboardService: IClipboardService; protected readonly _contextKeyService: IContextKeyService; protected readonly _notificationService: INotificationService; + protected readonly _hoverService: IHoverService; get editor() { return this._editor; @@ -113,7 +115,8 @@ export class CommonFindController extends Disposable implements IEditorContribut @IContextKeyService contextKeyService: IContextKeyService, @IStorageService storageService: IStorageService, @IClipboardService clipboardService: IClipboardService, - @INotificationService notificationService: INotificationService + @INotificationService notificationService: INotificationService, + @IHoverService hoverService: IHoverService ) { super(); this._editor = editor; @@ -122,6 +125,7 @@ export class CommonFindController extends Disposable implements IEditorContribut this._storageService = storageService; this._clipboardService = clipboardService; this._notificationService = notificationService; + this._hoverService = hoverService; this._updateHistoryDelayer = new Delayer(500); this._state = this._register(new FindReplaceState()); @@ -448,8 +452,9 @@ export class FindController extends CommonFindController implements IFindControl @INotificationService notificationService: INotificationService, @IStorageService _storageService: IStorageService, @IClipboardService clipboardService: IClipboardService, + @IHoverService hoverService: IHoverService, ) { - super(editor, _contextKeyService, _storageService, clipboardService, notificationService); + super(editor, _contextKeyService, _storageService, clipboardService, notificationService, hoverService); this._widget = null; this._findOptionsWidget = null; } @@ -503,7 +508,7 @@ export class FindController extends CommonFindController implements IFindControl } private _createFindWidget() { - this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService)); + this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService, this._hoverService)); this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService)); } diff --git a/src/vs/editor/contrib/find/browser/findDecorations.ts b/src/vs/editor/contrib/find/browser/findDecorations.ts index 62bfb58fb9c..e5e9018464a 100644 --- a/src/vs/editor/contrib/find/browser/findDecorations.ts +++ b/src/vs/editor/contrib/find/browser/findDecorations.ts @@ -288,6 +288,7 @@ export class FindDecorations implements IDisposable { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 13, className: 'currentFindMatch', + inlineClassName: 'currentFindMatchInline', showIfCollapsed: true, overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), @@ -304,6 +305,7 @@ export class FindDecorations implements IDisposable { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 10, className: 'findMatch', + inlineClassName: 'findMatchInline', showIfCollapsed: true, overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), diff --git a/src/vs/editor/contrib/find/browser/findModel.ts b/src/vs/editor/contrib/find/browser/findModel.ts index a4611d37daf..a6958ce4970 100644 --- a/src/vs/editor/contrib/find/browser/findModel.ts +++ b/src/vs/editor/contrib/find/browser/findModel.ts @@ -97,7 +97,12 @@ export class FindModelBoundToEditorModel { this._decorations = new FindDecorations(editor); this._toDispose.add(this._decorations); - this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100); + this._updateDecorationsScheduler = new RunOnceScheduler(() => { + if (!this._editor.hasModel()) { + return; + } + return this.research(false); + }, 100); this._toDispose.add(this._updateDecorationsScheduler); this._toDispose.add(this._editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 8d80095419d..9645d4b54ba 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -35,7 +35,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { asCssVariable, contrastBorder, editorFindMatchHighlightBorder, editorFindRangeHighlightBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariable, contrastBorder, editorFindMatchForeground, editorFindMatchHighlightBorder, editorFindMatchHighlightForeground, editorFindRangeHighlightBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon, widgetClose } from 'vs/platform/theme/common/iconRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -43,14 +43,14 @@ import { isHighContrast } from 'vs/platform/theme/common/theme'; import { assertIsDefined } from 'vs/base/common/types'; import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Selection } from 'vs/editor/common/core/selection'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; -const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); const findCollapsedIcon = registerIcon('find-collapsed', Codicon.chevronRight, nls.localize('findCollapsedIcon', 'Icon to indicate that the editor find widget is collapsed.')); const findExpandedIcon = registerIcon('find-expanded', Codicon.chevronDown, nls.localize('findExpandedIcon', 'Icon to indicate that the editor find widget is expanded.')); +export const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); export const findReplaceIcon = registerIcon('find-replace', Codicon.replace, nls.localize('findReplaceIcon', 'Icon for \'Replace\' in the editor find widget.')); export const findReplaceAllIcon = registerIcon('find-replace-all', Codicon.replaceAll, nls.localize('findReplaceAllIcon', 'Icon for \'Replace All\' in the editor find widget.')); export const findPreviousMatchIcon = registerIcon('find-previous-match', Codicon.arrowUp, nls.localize('findPreviousMatchIcon', 'Icon for \'Find Previous\' in the editor find widget.')); @@ -172,6 +172,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL themeService: IThemeService, storageService: IStorageService, notificationService: INotificationService, + private readonly _hoverService: IHoverService, ) { super(); this._codeEditor = codeEditor; @@ -409,9 +410,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } // remove previous content - if (this._matchesCount.firstChild) { - this._matchesCount.removeChild(this._matchesCount.firstChild); - } + this._matchesCount.firstChild?.remove(); let label: string; if (this._state.matchesCount > 0) { @@ -1024,7 +1023,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL onTrigger: () => { assertIsDefined(this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction)).run().then(undefined, onUnexpectedError); } - })); + }, this._hoverService)); // Next button this._nextBtn = this._register(new SimpleButton({ @@ -1034,7 +1033,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL onTrigger: () => { assertIsDefined(this._codeEditor.getAction(FIND_IDS.NextMatchFindAction)).run().then(undefined, onUnexpectedError); } - })); + }, this._hoverService)); const findPart = document.createElement('div'); findPart.className = 'find-part'; @@ -1102,7 +1101,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } } } - })); + }, this._hoverService)); // Replace input this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { @@ -1165,7 +1164,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL e.preventDefault(); } } - })); + }, this._hoverService)); // Replace all button this._replaceAllBtn = this._register(new SimpleButton({ @@ -1175,7 +1174,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL onTrigger: () => { this._controller.replaceAll(); } - })); + }, this._hoverService)); const replacePart = document.createElement('div'); replacePart.className = 'replace-part'; @@ -1200,7 +1199,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } this._showViewZone(); } - })); + }, this._hoverService)); this._toggleReplaceBtn.setExpanded(this._isReplaceVisible); // Widget @@ -1324,7 +1323,10 @@ export class SimpleButton extends Widget { private readonly _opts: ISimpleButtonOpts; private readonly _domNode: HTMLElement; - constructor(opts: ISimpleButtonOpts) { + constructor( + opts: ISimpleButtonOpts, + hoverService: IHoverService + ) { super(); this._opts = opts; @@ -1341,7 +1343,7 @@ export class SimpleButton extends Widget { this._domNode.className = className; this._domNode.setAttribute('role', 'button'); this._domNode.setAttribute('aria-label', this._opts.label); - this._register(setupCustomHover(opts.hoverDelegate ?? getDefaultHoverDelegate('element'), this._domNode, this._opts.label)); + this._register(hoverService.setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('element'), this._domNode, this._opts.label)); this.onclick(this._domNode, (e) => { this._opts.onTrigger(); @@ -1405,4 +1407,12 @@ registerThemingParticipant((theme, collector) => { if (hcBorder) { collector.addRule(`.monaco-editor .find-widget { border: 1px solid ${hcBorder}; }`); } + const findMatchForeground = theme.getColor(editorFindMatchForeground); + if (findMatchForeground) { + collector.addRule(`.monaco-editor .findMatchInline { color: ${findMatchForeground}; }`); + } + const findMatchHighlightForeground = theme.getColor(editorFindMatchHighlightForeground); + if (findMatchHighlightForeground) { + collector.addRule(`.monaco-editor .currentFindMatchInline { color: ${findMatchHighlightForeground}; }`); + } }); diff --git a/src/vs/editor/contrib/find/test/browser/find.test.ts b/src/vs/editor/contrib/find/test/browser/find.test.ts index 466c39baf1e..580ea739b76 100644 --- a/src/vs/editor/contrib/find/test/browser/find.test.ts +++ b/src/vs/editor/contrib/find/test/browser/find.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/contrib/find/test/browser/findController.test.ts b/src/vs/editor/contrib/find/test/browser/findController.test.ts index 27717765fa3..49fef95baf8 100644 --- a/src/vs/editor/contrib/find/test/browser/findController.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Delayer } from 'vs/base/common/async'; import * as platform from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -18,6 +18,7 @@ import { CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/browser/findM import { withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -35,9 +36,10 @@ class TestFindController extends CommonFindController { @IContextKeyService contextKeyService: IContextKeyService, @IStorageService storageService: IStorageService, @IClipboardService clipboardService: IClipboardService, - @INotificationService notificationService: INotificationService + @INotificationService notificationService: INotificationService, + @IHoverService hoverService: IHoverService ) { - super(editor, contextKeyService, storageService, clipboardService, notificationService); + super(editor, contextKeyService, storageService, clipboardService, notificationService, hoverService); this._findInputFocused = CONTEXT_FIND_INPUT_FOCUSED.bindTo(contextKeyService); this._updateHistoryDelayer = new Delayer(50); this.hasFocus = false; diff --git a/src/vs/editor/contrib/find/test/browser/findModel.test.ts b/src/vs/editor/contrib/find/test/browser/findModel.test.ts index 7d1ae5f5bd5..8cc753388b9 100644 --- a/src/vs/editor/contrib/find/test/browser/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; diff --git a/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts b/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts index cc9e76c93b5..1f534bbdae3 100644 --- a/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseReplaceString, ReplacePattern, ReplacePiece } from 'vs/editor/contrib/find/browser/replacePattern'; diff --git a/src/vs/editor/contrib/folding/browser/folding.css b/src/vs/editor/contrib/folding/browser/folding.css index f973d5f7a30..5f7ab05db78 100644 --- a/src/vs/editor/contrib/folding/browser/folding.css +++ b/src/vs/editor/contrib/folding/browser/folding.css @@ -31,7 +31,7 @@ } .monaco-editor .inline-folded:after { - color: grey; + color: var(--vscode-editor-foldPlaceholderForeground); margin: 0.1em 0.2em 0 0.2em; content: "\22EF"; /* ellipses unicode character */ display: inline; @@ -49,4 +49,3 @@ .monaco-editor .cldr.codicon.codicon-folding-manual-collapsed { color: var(--vscode-editorGutter-foldingControlForeground) !important; } - diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index ee2d344a7ea..25988ac7370 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -809,6 +809,30 @@ class FoldRecursivelyAction extends FoldingAction { } } + +class ToggleFoldRecursivelyAction extends FoldingAction { + + constructor() { + super({ + id: 'editor.toggleFoldRecursively', + label: nls.localize('toggleFoldRecursivelyAction.label', "Toggle Fold Recursively"), + alias: 'Toggle Fold Recursively', + precondition: CONTEXT_FOLDING_ENABLED, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL), + weight: KeybindingWeight.EditorContrib + } + }); + } + + invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { + const selectedLines = this.getSelectedLines(editor); + toggleCollapseState(foldingModel, Number.MAX_VALUE, selectedLines); + } +} + + class FoldAllBlockCommentsAction extends FoldingAction { constructor() { @@ -1127,7 +1151,7 @@ class FoldRangeFromSelectionAction extends FoldingAction { --endLineNumber; } if (endLineNumber > selection.startLineNumber) { - collapseRanges.push({ + collapseRanges.push({ startLineNumber: selection.startLineNumber, endLineNumber: endLineNumber, type: undefined, @@ -1189,6 +1213,7 @@ registerEditorAction(UnfoldAction); registerEditorAction(UnFoldRecursivelyAction); registerEditorAction(FoldAction); registerEditorAction(FoldRecursivelyAction); +registerEditorAction(ToggleFoldRecursivelyAction); registerEditorAction(FoldAllAction); registerEditorAction(UnfoldAllAction); registerEditorAction(FoldAllBlockCommentsAction); diff --git a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts index 03a9b4a402c..2350f9aad34 100644 --- a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts @@ -15,7 +15,8 @@ import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; const foldBackground = registerColor('editor.foldBackground', { light: transparent(editorSelectionBackground, 0.3), dark: transparent(editorSelectionBackground, 0.3), hcDark: null, hcLight: null }, localize('foldBackgroundBackground', "Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations."), true); -registerColor('editorGutter.foldingControlForeground', { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.')); +registerColor('editor.foldPlaceholderForeground', { light: '#808080', dark: '#808080', hcDark: null, hcLight: null }, localize('collapsedTextColor', "Color of the collapsed text after the first line of a folded range.")); +registerColor('editorGutter.foldingControlForeground', iconForeground, localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.')); export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown, localize('foldingExpandedIcon', 'Icon for expanded ranges in the editor glyph margin.')); export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight, localize('foldingCollapsedIcon', 'Icon for collapsed ranges in the editor glyph margin.')); diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index 5c800345b48..ee23abbfc74 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -246,7 +246,7 @@ export class FoldingRegions { } public toFoldRange(index: number): FoldRange { - return { + return { startLineNumber: this._startIndexes[index] & MAX_LINE_NUMBER, endLineNumber: this._endIndexes[index] & MAX_LINE_NUMBER, type: this._types ? this._types[index] : undefined, diff --git a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts index 3cef0c2a56e..1ea615a5bd9 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts index 912cf515d1a..a1a6dbe1dc1 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { MAX_FOLDING_REGIONS, FoldRange, FoldingRegions, FoldSource } from 'vs/editor/contrib/folding/browser/foldingRanges'; diff --git a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts index 71281efa963..6d57dec4e78 100644 --- a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { IRange } from 'vs/editor/common/core/range'; import { FoldingModel } from 'vs/editor/contrib/folding/browser/foldingModel'; import { HiddenRangeModel } from 'vs/editor/contrib/folding/browser/hiddenRangeModel'; diff --git a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts index afee83dff29..25db0e6d15d 100644 --- a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts b/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts index 0a1c3220e64..837dd6c0bc4 100644 --- a/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; diff --git a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts index 3551f446606..3b2bdb0648e 100644 --- a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModel } from 'vs/editor/common/model'; import { FoldingContext, FoldingRange, FoldingRangeProvider, ProviderResult } from 'vs/editor/common/languages'; diff --git a/src/vs/editor/contrib/format/browser/format.ts b/src/vs/editor/contrib/format/browser/format.ts index dbf6ede9eae..bb449bb7934 100644 --- a/src/vs/editor/contrib/format/browser/format.ts +++ b/src/vs/editor/contrib/format/browser/format.ts @@ -414,6 +414,23 @@ export async function getDocumentFormattingEditsUntilResult( return undefined; } +export async function getDocumentFormattingEditsWithSelectedProvider( + workerService: IEditorWorkerService, + languageFeaturesService: ILanguageFeaturesService, + editorOrModel: ITextModel | IActiveCodeEditor, + mode: FormattingMode, + token: CancellationToken, +): Promise { + const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel; + const provider = getRealAndSyntheticDocumentFormattersOrdered(languageFeaturesService.documentFormattingEditProvider, languageFeaturesService.documentRangeFormattingEditProvider, model); + const selected = await FormattingConflicts.select(provider, model, mode, FormattingKind.File); + if (selected) { + const rawEdits = await Promise.resolve(selected.provideDocumentFormattingEdits(model, model.getOptions(), token)).catch(onUnexpectedExternalError); + return await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + } + return undefined; +} + export function getOnTypeFormattingEdits( workerService: IEditorWorkerService, languageFeaturesService: ILanguageFeaturesService, diff --git a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts index 24ffac212bd..cf0e5c7d6d0 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts @@ -308,10 +308,9 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._disposables.add(this._actionbarWidget!.actionRunner.onWillRun(e => this.editor.focus())); const actions: IAction[] = []; - const menu = this._menuService.createMenu(MarkerNavigationWidget.TitleMenu, this._contextKeyService); - createAndFillInActionBarActions(menu, undefined, actions); + const menu = this._menuService.getMenuActions(MarkerNavigationWidget.TitleMenu, this._contextKeyService); + createAndFillInActionBarActions(menu, actions); this._actionbarWidget!.push(actions, { label: false, icon: true, index: 0 }); - menu.dispose(); } protected override _fillTitleIcon(container: HTMLElement): void { @@ -410,4 +409,4 @@ const editorMarkerNavigationWarningHeader = registerColor('editorMarkerNavigatio const editorMarkerNavigationInfo = registerColor('editorMarkerNavigationInfo.background', { dark: infoDefault, light: infoDefault, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorMarkerNavigationInfo', 'Editor marker navigation widget info color.')); const editorMarkerNavigationInfoHeader = registerColor('editorMarkerNavigationInfo.headerBackground', { dark: transparent(editorMarkerNavigationInfo, .1), light: transparent(editorMarkerNavigationInfo, .1), hcDark: null, hcLight: null }, nls.localize('editorMarkerNavigationInfoHeaderBackground', 'Editor marker navigation widget info heading background.')); -const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: editorBackground, light: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); +const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', editorBackground, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts index aa6b7865439..63541f54790 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts @@ -41,12 +41,12 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { Iterable } from 'vs/base/common/iterator'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; -MenuRegistry.appendMenuItem(MenuId.EditorContext, { +MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextPeek, title: nls.localize('peek.submenu', "Peek"), group: 'navigation', order: 100 -}); +} satisfies ISubmenuItem); export interface SymbolNavigationActionConfig { openToSide: boolean; @@ -253,7 +253,7 @@ export abstract class SymbolNavigationAction extends EditorAction2 { export class DefinitionAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, token), nls.localize('def.title', 'Definitions')); + return new ReferencesModel(await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, false, token), nls.localize('def.title', 'Definitions')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -380,7 +380,7 @@ registerAction2(class PeekDefinitionAction extends DefinitionAction { class DeclarationAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, token), nls.localize('decl.title', 'Declarations')); + return new ReferencesModel(await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, false, token), nls.localize('decl.title', 'Declarations')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -467,7 +467,7 @@ registerAction2(class PeekDeclarationAction extends DeclarationAction { class TypeDefinitionAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, token), nls.localize('typedef.title', 'Type Definitions')); + return new ReferencesModel(await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, false, token), nls.localize('typedef.title', 'Type Definitions')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -553,7 +553,7 @@ registerAction2(class PeekTypeDefinitionAction extends TypeDefinitionAction { class ImplementationAction extends SymbolNavigationAction { protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, token), nls.localize('impl.title', 'Implementations')); + return new ReferencesModel(await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, false, token), nls.localize('impl.title', 'Implementations')); } protected _getNoResultFoundMessage(info: IWordAtPosition | null): string { @@ -695,7 +695,7 @@ registerAction2(class GoToReferencesAction extends ReferencesAction { } protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, true, token), nls.localize('ref.title', 'References')); + return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, true, false, token), nls.localize('ref.title', 'References')); } }); @@ -723,7 +723,7 @@ registerAction2(class PeekReferencesAction extends ReferencesAction { } protected async _getLocationModel(languageFeaturesService: ILanguageFeaturesService, model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise { - return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, token), nls.localize('ref.title', 'References')); + return new ReferencesModel(await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, false, token), nls.localize('ref.title', 'References')); } }); @@ -846,7 +846,7 @@ CommandsRegistry.registerCommand({ return undefined; } - const references = createCancelablePromise(token => getReferencesAtPosition(languageFeaturesService.referenceProvider, control.getModel(), corePosition.Position.lift(position), false, token).then(references => new ReferencesModel(references, nls.localize('ref.title', 'References')))); + const references = createCancelablePromise(token => getReferencesAtPosition(languageFeaturesService.referenceProvider, control.getModel(), corePosition.Position.lift(position), false, false, token).then(references => new ReferencesModel(references, nls.localize('ref.title', 'References')))); const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); return Promise.resolve(controller.toggleWidget(range, references, false)); }); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts index 02cc3acebab..1477a394036 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts @@ -6,6 +6,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { matchesSomeScheme, Schemas } from 'vs/base/common/network'; import { registerModelAndPositionCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; @@ -14,13 +15,28 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ReferencesModel } from 'vs/editor/contrib/gotoSymbol/browser/referencesModel'; +function shouldIncludeLocationLink(sourceModel: ITextModel, loc: LocationLink): boolean { + // Always allow the location if the request comes from a document with the same scheme. + if (loc.uri.scheme === sourceModel.uri.scheme) { + return true; + } + + // Otherwise filter out locations from internal schemes + if (matchesSomeScheme(loc.uri, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock, Schemas.vscodeChatCodeCompareBlock)) { + return false; + } + + return true; +} + async function getLocationLinks( model: ITextModel, position: Position, registry: LanguageFeatureRegistry, + recursive: boolean, provide: (provider: T, model: ITextModel, position: Position) => ProviderResult ): Promise { - const provider = registry.ordered(model); + const provider = registry.ordered(model, recursive); // get results const promises = provider.map((provider): Promise => { @@ -31,40 +47,40 @@ async function getLocationLinks( }); const values = await Promise.all(promises); - return coalesce(values.flat()); + return coalesce(values.flat()).filter(loc => shouldIncludeLocationLink(model, loc)); } -export function getDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideDefinition(model, position, token); }); } -export function getDeclarationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getDeclarationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideDeclaration(model, position, token); }); } -export function getImplementationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getImplementationsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideImplementation(model, position, token); }); } -export function getTypeDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, (provider, model, position) => { +export function getTypeDefinitionsAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, (provider, model, position) => { return provider.provideTypeDefinition(model, position, token); }); } -export function getReferencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, compact: boolean, token: CancellationToken): Promise { - return getLocationLinks(model, position, registry, async (provider, model, position) => { - const result = await provider.provideReferences(model, position, { includeDeclaration: true }, token); +export function getReferencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, compact: boolean, recursive: boolean, token: CancellationToken): Promise { + return getLocationLinks(model, position, registry, recursive, async (provider, model, position) => { + const result = (await provider.provideReferences(model, position, { includeDeclaration: true }, token))?.filter(ref => shouldIncludeLocationLink(model, ref)); if (!compact || !result || result.length !== 2) { return result; } - const resultWithoutDeclaration = await provider.provideReferences(model, position, { includeDeclaration: false }, token); + const resultWithoutDeclaration = (await provider.provideReferences(model, position, { includeDeclaration: false }, token))?.filter(ref => shouldIncludeLocationLink(model, ref)); if (resultWithoutDeclaration && resultWithoutDeclaration.length === 1) { return resultWithoutDeclaration; } @@ -84,30 +100,59 @@ async function _sortedAndDeduped(callback: () => Promise): Promi registerModelAndPositionCommand('_executeDefinitionProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, CancellationToken.None); + const promise = getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeDefinitionProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeTypeDefinitionProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, CancellationToken.None); + const promise = getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeTypeDefinitionProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeDeclarationProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, CancellationToken.None); + const promise = getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); +registerModelAndPositionCommand('_executeDeclarationProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeReferenceProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, CancellationToken.None); + const promise = getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeReferenceProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getReferencesAtPosition(languageFeaturesService.referenceProvider, model, position, false, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); registerModelAndPositionCommand('_executeImplementationProvider', (accessor, model, position) => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const promise = getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, CancellationToken.None); + const promise = getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, false, CancellationToken.None); + return _sortedAndDeduped(() => promise); +}); + +registerModelAndPositionCommand('_executeImplementationProvider_recursive', (accessor, model, position) => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const promise = getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, position, true, CancellationToken.None); return _sortedAndDeduped(() => promise); }); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts index f5a08f94c1e..097de76f71e 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts @@ -297,7 +297,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri return Promise.resolve(null); } - return getDefinitionsAtPosition(this.languageFeaturesService.definitionProvider, model, position, token); + return getDefinitionsAtPosition(this.languageFeaturesService.definitionProvider, model, position, false, token); } private gotoDefinition(position: Position, openToSide: boolean): Promise { diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index b80e75d47ec..6503b7df220 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -23,9 +23,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/languages'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, IdentityProvider, OneReferenceRenderer, StringRepresentationProvider, TreeElement } from 'vs/editor/contrib/gotoSymbol/browser/peek/referencesTree'; import * as peekView from 'vs/editor/contrib/peekView/browser/peekView'; @@ -35,7 +33,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; class DecorationsManager implements IDisposable { @@ -224,10 +221,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { @IInstantiationService private readonly _instantiationService: IInstantiationService, @peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService, @ILabelService private readonly _uriLabel: ILabelService, - @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ILanguageService private readonly _languageService: ILanguageService, - @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, supportOnTitleClick: true }, _instantiationService); @@ -315,7 +309,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { }; this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, {}, this.editor); dom.hide(this._previewContainer); - this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null, this._undoRedoService, this._languageService, this._languageConfigurationService); + this._previewNotAvailableMessage = this._instantiationService.createInstance(TextModel, nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null); // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); @@ -390,7 +384,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { this._onDidSelectReference.fire({ element, kind, source: 'tree' }); } }; - this._tree.onDidOpen(e => { + this._disposables.add(this._tree.onDidOpen(e => { if (e.sideBySide) { onEvent(e.element, 'side'); } else if (e.editorOptions.pinned) { @@ -398,7 +392,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { } else { onEvent(e.element, 'show'); } - }); + })); dom.hide(this._treeContainer); } diff --git a/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts b/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts index d1195449779..a547f9450e1 100644 --- a/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts +++ b/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts deleted file mode 100644 index 491ecb001dd..00000000000 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ /dev/null @@ -1,1105 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from 'vs/base/browser/dom'; -import { HoverAction, HoverWidget, getHoverAccessibleViewHint } from 'vs/base/browser/ui/hover/hoverWidget'; -import { coalesce } from 'vs/base/common/arrays'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidgetPosition, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { IModelDecoration, PositionAffinity } from 'vs/editor/common/model'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { TokenizationRegistry } from 'vs/editor/common/languages'; -import { HoverOperation, HoverStartMode, HoverStartSource, IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation'; -import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverColorPickerWidget, IEditorHoverAction, IEditorHoverParticipant, IEditorHoverRenderContext, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AsyncIterableObject } from 'vs/base/common/async'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ResizableContentWidget } from 'vs/editor/contrib/hover/browser/resizableContentWidget'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; - -const $ = dom.$; - -export class ContentHoverController extends Disposable { - - private _currentResult: HoverResult | null = null; - - private readonly _computer: ContentHoverComputer; - private readonly _widget: ContentHoverWidget; - private readonly _participants: IEditorHoverParticipant[]; - private readonly _hoverOperation: HoverOperation; - - constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - ) { - super(); - - this._widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); - - // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order. - this._participants = []; - for (const participant of HoverParticipantRegistry.getAll()) { - this._participants.push(this._instantiationService.createInstance(participant, this._editor)); - } - this._participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); - - this._computer = new ContentHoverComputer(this._editor, this._participants); - this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer)); - - this._register(this._hoverOperation.onResult((result) => { - if (!this._computer.anchor) { - // invalid state, ignore result - return; - } - const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result.value) : result.value); - this._withResult(new HoverResult(this._computer.anchor, messages, result.isComplete)); - })); - this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => { - if (e.equals(KeyCode.Escape)) { - this.hide(); - } - })); - this._register(TokenizationRegistry.onDidChange(() => { - if (this._widget.position && this._currentResult) { - this._setCurrentResult(this._currentResult); // render again - } - })); - } - - /** - * Returns true if the hover shows now or will show. - */ - private _startShowingOrUpdateHover( - anchor: HoverAnchor | null, - mode: HoverStartMode, - source: HoverStartSource, - focus: boolean, - mouseEvent: IEditorMouseEvent | null - ): boolean { - - if (!this._widget.position || !this._currentResult) { - // The hover is not visible - if (anchor) { - this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); - return true; - } - return false; - } - - // The hover is currently visible - const isHoverSticky = this._editor.getOption(EditorOption.hover).sticky; - const isGettingCloser = ( - isHoverSticky - && mouseEvent - && this._widget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy) - ); - - if (isGettingCloser) { - // The mouse is getting closer to the hover, so we will keep the hover untouched - // But we will kick off a hover update at the new anchor, insisting on keeping the hover visible. - if (anchor) { - this._startHoverOperationIfNecessary(anchor, mode, source, focus, true); - } - return true; - } - - if (!anchor) { - this._setCurrentResult(null); - return false; - } - - if (anchor && this._currentResult.anchor.equals(anchor)) { - // The widget is currently showing results for the exact same anchor, so no update is needed - return true; - } - - if (!anchor.canAdoptVisibleHover(this._currentResult.anchor, this._widget.position)) { - // The new anchor is not compatible with the previous anchor - this._setCurrentResult(null); - this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); - return true; - } - - // We aren't getting any closer to the hover, so we will filter existing results - // and keep those which also apply to the new anchor. - this._setCurrentResult(this._currentResult.filter(anchor)); - this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); - return true; - } - - private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, source: HoverStartSource, focus: boolean, insistOnKeepingHoverVisible: boolean): void { - - if (this._computer.anchor && this._computer.anchor.equals(anchor)) { - // We have to start a hover operation at the exact same anchor as before, so no work is needed - return; - } - this._hoverOperation.cancel(); - this._computer.anchor = anchor; - this._computer.shouldFocus = focus; - this._computer.source = source; - this._computer.insistOnKeepingHoverVisible = insistOnKeepingHoverVisible; - this._hoverOperation.start(mode); - } - - private _setCurrentResult(hoverResult: HoverResult | null): void { - - if (this._currentResult === hoverResult) { - // avoid updating the DOM to avoid resetting the user selection - return; - } - if (hoverResult && hoverResult.messages.length === 0) { - hoverResult = null; - } - this._currentResult = hoverResult; - if (this._currentResult) { - this._renderMessages(this._currentResult.anchor, this._currentResult.messages); - } else { - this._widget.hide(); - } - } - - private _addLoadingMessage(result: IHoverPart[]): IHoverPart[] { - if (this._computer.anchor) { - for (const participant of this._participants) { - if (participant.createLoadingMessage) { - const loadingMessage = participant.createLoadingMessage(this._computer.anchor); - if (loadingMessage) { - return result.slice(0).concat([loadingMessage]); - } - } - } - } - return result; - } - - private _withResult(hoverResult: HoverResult): void { - if (this._widget.position && this._currentResult && this._currentResult.isComplete) { - // The hover is visible with a previous complete result. - - if (!hoverResult.isComplete) { - // Instead of rendering the new partial result, we wait for the result to be complete. - return; - } - - if (this._computer.insistOnKeepingHoverVisible && hoverResult.messages.length === 0) { - // The hover would now hide normally, so we'll keep the previous messages - return; - } - } - - this._setCurrentResult(hoverResult); - } - - private _renderMessages(anchor: HoverAnchor, messages: IHoverPart[]): void { - const { showAtPosition, showAtSecondaryPosition, highlightRange } = ContentHoverController.computeHoverRanges(this._editor, anchor.range, messages); - - const disposables = new DisposableStore(); - const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService)); - const fragment = document.createDocumentFragment(); - - let colorPicker: IEditorHoverColorPickerWidget | null = null; - const context: IEditorHoverRenderContext = { - fragment, - statusBar, - setColorPicker: (widget) => colorPicker = widget, - onContentsChanged: () => this._widget.onContentsChanged(), - setMinimumDimensions: (dimensions: dom.Dimension) => this._widget.setMinimumDimensions(dimensions), - hide: () => this.hide() - }; - - for (const participant of this._participants) { - const hoverParts = messages.filter(msg => msg.owner === participant); - if (hoverParts.length > 0) { - disposables.add(participant.renderHoverParts(context, hoverParts)); - } - } - - const isBeforeContent = messages.some(m => m.isBeforeContent); - - if (statusBar.hasContent) { - fragment.appendChild(statusBar.hoverElement); - } - - if (fragment.hasChildNodes()) { - if (highlightRange) { - const highlightDecoration = this._editor.createDecorationsCollection(); - highlightDecoration.set([{ - range: highlightRange, - options: ContentHoverController._DECORATION_OPTIONS - }]); - disposables.add(toDisposable(() => { - highlightDecoration.clear(); - })); - } - - this._widget.showAt(fragment, new ContentHoverVisibleData( - anchor.initialMousePosX, - anchor.initialMousePosY, - colorPicker, - showAtPosition, - showAtSecondaryPosition, - this._editor.getOption(EditorOption.hover).above, - this._computer.shouldFocus, - this._computer.source, - isBeforeContent, - disposables - )); - } else { - disposables.dispose(); - } - } - - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ - description: 'content-hover-highlight', - className: 'hoverHighlight' - }); - - public static computeHoverRanges(editor: ICodeEditor, anchorRange: Range, messages: IHoverPart[]) { - - let startColumnBoundary = 1; - if (editor.hasModel()) { - // Ensure the range is on the current view line - const viewModel = editor._getViewModel(); - const coordinatesConverter = viewModel.coordinatesConverter; - const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange); - const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, viewModel.getLineMinColumn(anchorViewRange.startLineNumber)); - startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column; - } - - // The anchor range is always on a single line - const anchorLineNumber = anchorRange.startLineNumber; - let renderStartColumn = anchorRange.startColumn; - let highlightRange = messages[0].range; - let forceShowAtRange = null; - - for (const msg of messages) { - highlightRange = Range.plusRange(highlightRange, msg.range); - if (msg.range.startLineNumber === anchorLineNumber && msg.range.endLineNumber === anchorLineNumber) { - // this message has a range that is completely sitting on the line of the anchor - renderStartColumn = Math.max(Math.min(renderStartColumn, msg.range.startColumn), startColumnBoundary); - } - if (msg.forceShowAtRange) { - forceShowAtRange = msg.range; - } - } - - const showAtPosition = forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, anchorRange.startColumn); - const showAtSecondaryPosition = forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, renderStartColumn); - - return { - showAtPosition, - showAtSecondaryPosition, - highlightRange - }; - } - - /** - * Returns true if the hover shows now or will show. - */ - public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean { - - if (this._widget.isResizing) { - return true; - } - - const anchorCandidates: HoverAnchor[] = []; - for (const participant of this._participants) { - if (participant.suggestHoverAnchor) { - const anchor = participant.suggestHoverAnchor(mouseEvent); - if (anchor) { - anchorCandidates.push(anchor); - } - } - } - - const target = mouseEvent.target; - - if (target.type === MouseTargetType.CONTENT_TEXT) { - anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); - } - - if (target.type === MouseTargetType.CONTENT_EMPTY) { - const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; - if ( - !target.detail.isAfterLines - && typeof target.detail.horizontalDistanceToText === 'number' - && target.detail.horizontalDistanceToText < epsilon - ) { - // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough - anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); - } - } - - if (anchorCandidates.length === 0) { - return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); - } - - anchorCandidates.sort((a, b) => b.priority - a.priority); - return this._startShowingOrUpdateHover(anchorCandidates[0], HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); - } - - public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void { - this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null); - } - - public getWidgetContent(): string | undefined { - const node = this._widget.getDomNode(); - if (!node.textContent) { - return undefined; - } - return node.textContent; - } - - public containsNode(node: Node | null | undefined): boolean { - return (node ? this._widget.getDomNode().contains(node) : false); - } - - public focus(): void { - this._widget.focus(); - } - - public scrollUp(): void { - this._widget.scrollUp(); - } - - public scrollDown(): void { - this._widget.scrollDown(); - } - - public scrollLeft(): void { - this._widget.scrollLeft(); - } - - public scrollRight(): void { - this._widget.scrollRight(); - } - - public pageUp(): void { - this._widget.pageUp(); - } - - public pageDown(): void { - this._widget.pageDown(); - } - - public goToTop(): void { - this._widget.goToTop(); - } - - public goToBottom(): void { - this._widget.goToBottom(); - } - - public hide(): void { - this._computer.anchor = null; - this._hoverOperation.cancel(); - this._setCurrentResult(null); - } - - public get isColorPickerVisible(): boolean { - return this._widget.isColorPickerVisible; - } - - public get isVisibleFromKeyboard(): boolean { - return this._widget.isVisibleFromKeyboard; - } - - public get isVisible(): boolean { - return this._widget.isVisible; - } - - public get isFocused(): boolean { - return this._widget.isFocused; - } - - public get isResizing(): boolean { - return this._widget.isResizing; - } - - public get widget() { - return this._widget; - } -} - -class HoverResult { - - constructor( - public readonly anchor: HoverAnchor, - public readonly messages: IHoverPart[], - public readonly isComplete: boolean - ) { } - - public filter(anchor: HoverAnchor): HoverResult { - const filteredMessages = this.messages.filter((m) => m.isValidForHoverAnchor(anchor)); - if (filteredMessages.length === this.messages.length) { - return this; - } - return new FilteredHoverResult(this, this.anchor, filteredMessages, this.isComplete); - } -} - -class FilteredHoverResult extends HoverResult { - - constructor( - private readonly original: HoverResult, - anchor: HoverAnchor, - messages: IHoverPart[], - isComplete: boolean - ) { - super(anchor, messages, isComplete); - } - - public override filter(anchor: HoverAnchor): HoverResult { - return this.original.filter(anchor); - } -} - -class ContentHoverVisibleData { - - public closestMouseDistance: number | undefined = undefined; - - constructor( - public initialMousePosX: number | undefined, - public initialMousePosY: number | undefined, - public readonly colorPicker: IEditorHoverColorPickerWidget | null, - public readonly showAtPosition: Position, - public readonly showAtSecondaryPosition: Position, - public readonly preferAbove: boolean, - public readonly stoleFocus: boolean, - public readonly source: HoverStartSource, - public readonly isBeforeContent: boolean, - public readonly disposables: DisposableStore - ) { } -} - -const HORIZONTAL_SCROLLING_BY = 30; -const SCROLLBAR_WIDTH = 10; -const CONTAINER_HEIGHT_PADDING = 6; - -export class ContentHoverWidget extends ResizableContentWidget { - - public static ID = 'editor.contrib.resizableContentHoverWidget'; - private static _lastDimensions: dom.Dimension = new dom.Dimension(0, 0); - - private _visibleData: ContentHoverVisibleData | undefined; - private _positionPreference: ContentWidgetPositionPreference | undefined; - private _minimumSize: dom.Dimension; - private _contentWidth: number | undefined; - - private readonly _hover: HoverWidget = this._register(new HoverWidget()); - private readonly _hoverVisibleKey: IContextKey; - private readonly _hoverFocusedKey: IContextKey; - - public get isColorPickerVisible(): boolean { - return Boolean(this._visibleData?.colorPicker); - } - - public get isVisibleFromKeyboard(): boolean { - return (this._visibleData?.source === HoverStartSource.Keyboard); - } - - public get isVisible(): boolean { - return this._hoverVisibleKey.get() ?? false; - } - - public get isFocused(): boolean { - return this._hoverFocusedKey.get() ?? false; - } - - constructor( - editor: ICodeEditor, - @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IKeybindingService private readonly _keybindingService: IKeybindingService - ) { - const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8; - const minimumWidth = 150; - const minimumSize = new dom.Dimension(minimumWidth, minimumHeight); - super(editor, minimumSize); - - this._minimumSize = minimumSize; - this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(contextKeyService); - this._hoverFocusedKey = EditorContextKeys.hoverFocused.bindTo(contextKeyService); - - dom.append(this._resizableNode.domNode, this._hover.containerDomNode); - this._resizableNode.domNode.style.zIndex = '50'; - - this._register(this._editor.onDidLayoutChange(() => { - if (this.isVisible) { - this._updateMaxDimensions(); - } - })); - this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { - if (e.hasChanged(EditorOption.fontInfo)) { - this._updateFont(); - } - })); - const focusTracker = this._register(dom.trackFocus(this._resizableNode.domNode)); - this._register(focusTracker.onDidFocus(() => { - this._hoverFocusedKey.set(true); - })); - this._register(focusTracker.onDidBlur(() => { - this._hoverFocusedKey.set(false); - })); - this._setHoverData(undefined); - this._editor.addContentWidget(this); - } - - public override dispose(): void { - super.dispose(); - this._visibleData?.disposables.dispose(); - this._editor.removeContentWidget(this); - } - - public getId(): string { - return ContentHoverWidget.ID; - } - - private static _applyDimensions(container: HTMLElement, width: number | string, height: number | string): void { - const transformedWidth = typeof width === 'number' ? `${width}px` : width; - const transformedHeight = typeof height === 'number' ? `${height}px` : height; - container.style.width = transformedWidth; - container.style.height = transformedHeight; - } - - private _setContentsDomNodeDimensions(width: number | string, height: number | string): void { - const contentsDomNode = this._hover.contentsDomNode; - return ContentHoverWidget._applyDimensions(contentsDomNode, width, height); - } - - private _setContainerDomNodeDimensions(width: number | string, height: number | string): void { - const containerDomNode = this._hover.containerDomNode; - return ContentHoverWidget._applyDimensions(containerDomNode, width, height); - } - - private _setHoverWidgetDimensions(width: number | string, height: number | string): void { - this._setContentsDomNodeDimensions(width, height); - this._setContainerDomNodeDimensions(width, height); - this._layoutContentWidget(); - } - - private static _applyMaxDimensions(container: HTMLElement, width: number | string, height: number | string) { - const transformedWidth = typeof width === 'number' ? `${width}px` : width; - const transformedHeight = typeof height === 'number' ? `${height}px` : height; - container.style.maxWidth = transformedWidth; - container.style.maxHeight = transformedHeight; - } - - private _setHoverWidgetMaxDimensions(width: number | string, height: number | string): void { - ContentHoverWidget._applyMaxDimensions(this._hover.contentsDomNode, width, height); - ContentHoverWidget._applyMaxDimensions(this._hover.containerDomNode, width, height); - this._hover.containerDomNode.style.setProperty('--vscode-hover-maxWidth', typeof width === 'number' ? `${width}px` : width); - this._layoutContentWidget(); - } - - private _hasHorizontalScrollbar(): boolean { - const scrollDimensions = this._hover.scrollbar.getScrollDimensions(); - const hasHorizontalScrollbar = scrollDimensions.scrollWidth > scrollDimensions.width; - return hasHorizontalScrollbar; - } - - private _adjustContentsBottomPadding(): void { - const contentsDomNode = this._hover.contentsDomNode; - const extraBottomPadding = `${this._hover.scrollbar.options.horizontalScrollbarSize}px`; - if (contentsDomNode.style.paddingBottom !== extraBottomPadding) { - contentsDomNode.style.paddingBottom = extraBottomPadding; - } - } - - private _setAdjustedHoverWidgetDimensions(size: dom.Dimension): void { - this._setHoverWidgetMaxDimensions('none', 'none'); - const width = size.width; - const height = size.height; - this._setHoverWidgetDimensions(width, height); - // measure if widget has horizontal scrollbar after setting the dimensions - if (this._hasHorizontalScrollbar()) { - this._adjustContentsBottomPadding(); - this._setContentsDomNodeDimensions(width, height - SCROLLBAR_WIDTH); - } - } - - private _updateResizableNodeMaxDimensions(): void { - const maxRenderingWidth = this._findMaximumRenderingWidth() ?? Infinity; - const maxRenderingHeight = this._findMaximumRenderingHeight() ?? Infinity; - this._resizableNode.maxSize = new dom.Dimension(maxRenderingWidth, maxRenderingHeight); - this._setHoverWidgetMaxDimensions(maxRenderingWidth, maxRenderingHeight); - } - - protected override _resize(size: dom.Dimension): void { - ContentHoverWidget._lastDimensions = new dom.Dimension(size.width, size.height); - this._setAdjustedHoverWidgetDimensions(size); - this._resizableNode.layout(size.height, size.width); - this._updateResizableNodeMaxDimensions(); - this._hover.scrollbar.scanDomNode(); - this._editor.layoutContentWidget(this); - this._visibleData?.colorPicker?.layout(); - } - - private _findAvailableSpaceVertically(): number | undefined { - const position = this._visibleData?.showAtPosition; - if (!position) { - return; - } - return this._positionPreference === ContentWidgetPositionPreference.ABOVE ? - this._availableVerticalSpaceAbove(position) - : this._availableVerticalSpaceBelow(position); - } - - private _findMaximumRenderingHeight(): number | undefined { - const availableSpace = this._findAvailableSpaceVertically(); - if (!availableSpace) { - return; - } - // Padding needed in order to stop the resizing down to a smaller height - let maximumHeight = CONTAINER_HEIGHT_PADDING; - Array.from(this._hover.contentsDomNode.children).forEach((hoverPart) => { - maximumHeight += hoverPart.clientHeight; - }); - - if (this._hasHorizontalScrollbar()) { - maximumHeight += SCROLLBAR_WIDTH; - } - return Math.min(availableSpace, maximumHeight); - } - - private _isHoverTextOverflowing(): boolean { - // To find out if the text is overflowing, we will disable wrapping, check the widths, and then re-enable wrapping - this._hover.containerDomNode.style.setProperty('--vscode-hover-whiteSpace', 'nowrap'); - this._hover.containerDomNode.style.setProperty('--vscode-hover-sourceWhiteSpace', 'nowrap'); - - const overflowing = Array.from(this._hover.contentsDomNode.children).some((hoverElement) => { - return hoverElement.scrollWidth > hoverElement.clientWidth; - }); - - this._hover.containerDomNode.style.removeProperty('--vscode-hover-whiteSpace'); - this._hover.containerDomNode.style.removeProperty('--vscode-hover-sourceWhiteSpace'); - - return overflowing; - } - - private _findMaximumRenderingWidth(): number | undefined { - if (!this._editor || !this._editor.hasModel()) { - return; - } - - const overflowing = this._isHoverTextOverflowing(); - const initialWidth = ( - typeof this._contentWidth === 'undefined' - ? 0 - : this._contentWidth - 2 // - 2 for the borders - ); - - if (overflowing || this._hover.containerDomNode.clientWidth < initialWidth) { - const bodyBoxWidth = dom.getClientArea(this._hover.containerDomNode.ownerDocument.body).width; - const horizontalPadding = 14; - return bodyBoxWidth - horizontalPadding; - } else { - return this._hover.containerDomNode.clientWidth + 2; - } - } - - public isMouseGettingCloser(posx: number, posy: number): boolean { - - if (!this._visibleData) { - return false; - } - if ( - typeof this._visibleData.initialMousePosX === 'undefined' - || typeof this._visibleData.initialMousePosY === 'undefined' - ) { - this._visibleData.initialMousePosX = posx; - this._visibleData.initialMousePosY = posy; - return false; - } - - const widgetRect = dom.getDomNodePagePosition(this.getDomNode()); - if (typeof this._visibleData.closestMouseDistance === 'undefined') { - this._visibleData.closestMouseDistance = computeDistanceFromPointToRectangle( - this._visibleData.initialMousePosX, - this._visibleData.initialMousePosY, - widgetRect.left, - widgetRect.top, - widgetRect.width, - widgetRect.height - ); - } - - const distance = computeDistanceFromPointToRectangle( - posx, - posy, - widgetRect.left, - widgetRect.top, - widgetRect.width, - widgetRect.height - ); - if (distance > this._visibleData.closestMouseDistance + 4 /* tolerance of 4 pixels */) { - // The mouse is getting farther away - return false; - } - - this._visibleData.closestMouseDistance = Math.min(this._visibleData.closestMouseDistance, distance); - return true; - } - - private _setHoverData(hoverData: ContentHoverVisibleData | undefined): void { - this._visibleData?.disposables.dispose(); - this._visibleData = hoverData; - this._hoverVisibleKey.set(!!hoverData); - this._hover.containerDomNode.classList.toggle('hidden', !hoverData); - } - - private _updateFont(): void { - const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); - const contentsDomNode = this._hover.contentsDomNode; - contentsDomNode.style.fontSize = `${fontSize}px`; - contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`; - const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); - codeClasses.forEach(node => this._editor.applyFontInfo(node)); - } - - private _updateContent(node: DocumentFragment): void { - const contentsDomNode = this._hover.contentsDomNode; - contentsDomNode.style.paddingBottom = ''; - contentsDomNode.textContent = ''; - contentsDomNode.appendChild(node); - } - - private _layoutContentWidget(): void { - this._editor.layoutContentWidget(this); - this._hover.onContentsChanged(); - } - - private _updateMaxDimensions() { - const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height); - const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 500, ContentHoverWidget._lastDimensions.width); - this._setHoverWidgetMaxDimensions(width, height); - } - - private _render(node: DocumentFragment, hoverData: ContentHoverVisibleData) { - this._setHoverData(hoverData); - this._updateFont(); - this._updateContent(node); - this._updateMaxDimensions(); - this.onContentsChanged(); - // Simply force a synchronous render on the editor - // such that the widget does not really render with left = '0px' - this._editor.render(); - } - - override getPosition(): IContentWidgetPosition | null { - if (!this._visibleData) { - return null; - } - return { - position: this._visibleData.showAtPosition, - secondaryPosition: this._visibleData.showAtSecondaryPosition, - positionAffinity: this._visibleData.isBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined, - preference: [this._positionPreference ?? ContentWidgetPositionPreference.ABOVE] - }; - } - - public showAt(node: DocumentFragment, hoverData: ContentHoverVisibleData): void { - if (!this._editor || !this._editor.hasModel()) { - return; - } - this._render(node, hoverData); - const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); - const widgetPosition = hoverData.showAtPosition; - this._positionPreference = this._findPositionPreference(widgetHeight, widgetPosition) ?? ContentWidgetPositionPreference.ABOVE; - - // See https://github.com/microsoft/vscode/issues/140339 - // TODO: Doing a second layout of the hover after force rendering the editor - this.onContentsChanged(); - if (hoverData.stoleFocus) { - this._hover.containerDomNode.focus(); - } - hoverData.colorPicker?.layout(); - // The aria label overrides the label, so if we add to it, add the contents of the hover - const hoverFocused = this._hover.containerDomNode.ownerDocument.activeElement === this._hover.containerDomNode; - const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint( - this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), - this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel() ?? '' - ); - - if (accessibleViewHint) { - this._hover.contentsDomNode.ariaLabel = this._hover.contentsDomNode.textContent + ', ' + accessibleViewHint; - } - } - - public hide(): void { - if (!this._visibleData) { - return; - } - const stoleFocus = this._visibleData.stoleFocus || this._hoverFocusedKey.get(); - this._setHoverData(undefined); - this._resizableNode.maxSize = new dom.Dimension(Infinity, Infinity); - this._resizableNode.clearSashHoverState(); - this._hoverFocusedKey.set(false); - this._editor.layoutContentWidget(this); - if (stoleFocus) { - this._editor.focus(); - } - } - - private _removeConstraintsRenderNormally(): void { - // Added because otherwise the initial size of the hover content is smaller than should be - const layoutInfo = this._editor.getLayoutInfo(); - this._resizableNode.layout(layoutInfo.height, layoutInfo.width); - this._setHoverWidgetDimensions('auto', 'auto'); - } - - private _adjustHoverHeightForScrollbar(height: number) { - const containerDomNode = this._hover.containerDomNode; - const contentsDomNode = this._hover.contentsDomNode; - const maxRenderingHeight = this._findMaximumRenderingHeight() ?? Infinity; - this._setContainerDomNodeDimensions(dom.getTotalWidth(containerDomNode), Math.min(maxRenderingHeight, height)); - this._setContentsDomNodeDimensions(dom.getTotalWidth(contentsDomNode), Math.min(maxRenderingHeight, height - SCROLLBAR_WIDTH)); - } - - public setMinimumDimensions(dimensions: dom.Dimension): void { - // We combine the new minimum dimensions with the previous ones - this._minimumSize = new dom.Dimension( - Math.max(this._minimumSize.width, dimensions.width), - Math.max(this._minimumSize.height, dimensions.height) - ); - this._updateMinimumWidth(); - } - - private _updateMinimumWidth(): void { - const width = ( - typeof this._contentWidth === 'undefined' - ? this._minimumSize.width - : Math.min(this._contentWidth, this._minimumSize.width) - ); - // We want to avoid that the hover is artificially large, so we use the content width as minimum width - this._resizableNode.minSize = new dom.Dimension(width, this._minimumSize.height); - } - - public onContentsChanged(): void { - this._removeConstraintsRenderNormally(); - const containerDomNode = this._hover.containerDomNode; - - let height = dom.getTotalHeight(containerDomNode); - let width = dom.getTotalWidth(containerDomNode); - this._resizableNode.layout(height, width); - - this._setHoverWidgetDimensions(width, height); - - height = dom.getTotalHeight(containerDomNode); - width = dom.getTotalWidth(containerDomNode); - this._contentWidth = width; - this._updateMinimumWidth(); - this._resizableNode.layout(height, width); - - if (this._hasHorizontalScrollbar()) { - this._adjustContentsBottomPadding(); - this._adjustHoverHeightForScrollbar(height); - } - if (this._visibleData?.showAtPosition) { - const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); - this._positionPreference = this._findPositionPreference(widgetHeight, this._visibleData.showAtPosition); - } - this._layoutContentWidget(); - } - - public focus(): void { - this._hover.containerDomNode.focus(); - } - - public scrollUp(): void { - const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; - const fontInfo = this._editor.getOption(EditorOption.fontInfo); - this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - fontInfo.lineHeight }); - } - - public scrollDown(): void { - const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; - const fontInfo = this._editor.getOption(EditorOption.fontInfo); - this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + fontInfo.lineHeight }); - } - - public scrollLeft(): void { - const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft; - this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft - HORIZONTAL_SCROLLING_BY }); - } - - public scrollRight(): void { - const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft; - this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft + HORIZONTAL_SCROLLING_BY }); - } - - public pageUp(): void { - const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; - const scrollHeight = this._hover.scrollbar.getScrollDimensions().height; - this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - scrollHeight }); - } - - public pageDown(): void { - const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; - const scrollHeight = this._hover.scrollbar.getScrollDimensions().height; - this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + scrollHeight }); - } - - public goToTop(): void { - this._hover.scrollbar.setScrollPosition({ scrollTop: 0 }); - } - - public goToBottom(): void { - this._hover.scrollbar.setScrollPosition({ scrollTop: this._hover.scrollbar.getScrollDimensions().scrollHeight }); - } -} - -export class EditorHoverStatusBar extends Disposable implements IEditorHoverStatusBar { - - public readonly hoverElement: HTMLElement; - private readonly actionsElement: HTMLElement; - private _hasContent: boolean = false; - - public get hasContent() { - return this._hasContent; - } - - constructor( - @IKeybindingService private readonly _keybindingService: IKeybindingService, - ) { - super(); - this.hoverElement = $('div.hover-row.status-bar'); - this.actionsElement = dom.append(this.hoverElement, $('div.actions')); - } - - public addAction( - actionOptions: { - label: string; - iconClass?: string; run: (target: HTMLElement) => void; - commandId: string; - }): IEditorHoverAction { - - const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); - const keybindingLabel = keybinding ? keybinding.getLabel() : null; - this._hasContent = true; - return this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel)); - } - - public append(element: HTMLElement): HTMLElement { - const result = dom.append(this.actionsElement, element); - this._hasContent = true; - return result; - } -} - -class ContentHoverComputer implements IHoverComputer { - - private _anchor: HoverAnchor | null = null; - public get anchor(): HoverAnchor | null { return this._anchor; } - public set anchor(value: HoverAnchor | null) { this._anchor = value; } - - private _shouldFocus: boolean = false; - public get shouldFocus(): boolean { return this._shouldFocus; } - public set shouldFocus(value: boolean) { this._shouldFocus = value; } - - private _source: HoverStartSource = HoverStartSource.Mouse; - public get source(): HoverStartSource { return this._source; } - public set source(value: HoverStartSource) { this._source = value; } - - private _insistOnKeepingHoverVisible: boolean = false; - public get insistOnKeepingHoverVisible(): boolean { return this._insistOnKeepingHoverVisible; } - public set insistOnKeepingHoverVisible(value: boolean) { this._insistOnKeepingHoverVisible = value; } - - constructor( - private readonly _editor: ICodeEditor, - private readonly _participants: readonly IEditorHoverParticipant[] - ) { - } - - private static _getLineDecorations(editor: IActiveCodeEditor, anchor: HoverAnchor): IModelDecoration[] { - if (anchor.type !== HoverAnchorType.Range && !anchor.supportsMarkerHover) { - return []; - } - - const model = editor.getModel(); - const lineNumber = anchor.range.startLineNumber; - - if (lineNumber > model.getLineCount()) { - // invalid line - return []; - } - - const maxColumn = model.getLineMaxColumn(lineNumber); - - return editor.getLineDecorations(lineNumber).filter((d) => { - if (d.options.isWholeLine) { - return true; - } - - const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; - const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; - - if (d.options.showIfCollapsed) { - // Relax check around `showIfCollapsed` decorations to also include +/- 1 character - if (startColumn > anchor.range.startColumn + 1 || anchor.range.endColumn - 1 > endColumn) { - return false; - } - } else { - if (startColumn > anchor.range.startColumn || anchor.range.endColumn > endColumn) { - return false; - } - } - - return true; - }); - } - - public computeAsync(token: CancellationToken): AsyncIterableObject { - const anchor = this._anchor; - - if (!this._editor.hasModel() || !anchor) { - return AsyncIterableObject.EMPTY; - } - - const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, anchor); - - return AsyncIterableObject.merge( - this._participants.map((participant) => { - if (!participant.computeAsync) { - return AsyncIterableObject.EMPTY; - } - return participant.computeAsync(anchor, lineDecorations, token); - }) - ); - } - - public computeSync(): IHoverPart[] { - if (!this._editor.hasModel() || !this._anchor) { - return []; - } - - const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, this._anchor); - - let result: IHoverPart[] = []; - for (const participant of this._participants) { - result = result.concat(participant.computeSync(this._anchor, lineDecorations)); - } - - return coalesce(result); - } -} - -function computeDistanceFromPointToRectangle(pointX: number, pointY: number, left: number, top: number, width: number, height: number): number { - const x = (left + width / 2); // x center of rectangle - const y = (top + height / 2); // y center of rectangle - const dx = Math.max(Math.abs(pointX - x) - width / 2, 0); - const dy = Math.max(Math.abs(pointY - y) - height / 2, 0); - return Math.sqrt(dx * dx + dy * dy); -} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverComputer.ts b/src/vs/editor/contrib/hover/browser/contentHoverComputer.ts new file mode 100644 index 00000000000..d5d0e6c85fd --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverComputer.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { coalesce } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { HoverStartSource, IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { AsyncIterableObject } from 'vs/base/common/async'; + +export class ContentHoverComputer implements IHoverComputer { + + private _anchor: HoverAnchor | null = null; + public get anchor(): HoverAnchor | null { return this._anchor; } + public set anchor(value: HoverAnchor | null) { this._anchor = value; } + + private _shouldFocus: boolean = false; + public get shouldFocus(): boolean { return this._shouldFocus; } + public set shouldFocus(value: boolean) { this._shouldFocus = value; } + + private _source: HoverStartSource = HoverStartSource.Mouse; + public get source(): HoverStartSource { return this._source; } + public set source(value: HoverStartSource) { this._source = value; } + + private _insistOnKeepingHoverVisible: boolean = false; + public get insistOnKeepingHoverVisible(): boolean { return this._insistOnKeepingHoverVisible; } + public set insistOnKeepingHoverVisible(value: boolean) { this._insistOnKeepingHoverVisible = value; } + + constructor( + private readonly _editor: ICodeEditor, + private readonly _participants: readonly IEditorHoverParticipant[] + ) { + } + + private static _getLineDecorations(editor: IActiveCodeEditor, anchor: HoverAnchor): IModelDecoration[] { + if (anchor.type !== HoverAnchorType.Range && !anchor.supportsMarkerHover) { + return []; + } + + const model = editor.getModel(); + const lineNumber = anchor.range.startLineNumber; + + if (lineNumber > model.getLineCount()) { + // invalid line + return []; + } + + const maxColumn = model.getLineMaxColumn(lineNumber); + + return editor.getLineDecorations(lineNumber).filter((d) => { + if (d.options.isWholeLine) { + return true; + } + + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; + const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; + + if (d.options.showIfCollapsed) { + // Relax check around `showIfCollapsed` decorations to also include +/- 1 character + if (startColumn > anchor.range.startColumn + 1 || anchor.range.endColumn - 1 > endColumn) { + return false; + } + } else { + if (startColumn > anchor.range.startColumn || anchor.range.endColumn > endColumn) { + return false; + } + } + + return true; + }); + } + + public computeAsync(token: CancellationToken): AsyncIterableObject { + const anchor = this._anchor; + + if (!this._editor.hasModel() || !anchor) { + return AsyncIterableObject.EMPTY; + } + + const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, anchor); + + return AsyncIterableObject.merge( + this._participants.map((participant) => { + if (!participant.computeAsync) { + return AsyncIterableObject.EMPTY; + } + return participant.computeAsync(anchor, lineDecorations, token); + }) + ); + } + + public computeSync(): IHoverPart[] { + if (!this._editor.hasModel() || !this._anchor) { + return []; + } + + const lineDecorations = ContentHoverComputer._getLineDecorations(this._editor, this._anchor); + + let result: IHoverPart[] = []; + for (const participant of this._participants) { + result = result.concat(participant.computeSync(this._anchor, lineDecorations)); + } + + return coalesce(result); + } +} + diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts new file mode 100644 index 00000000000..4664ec67af3 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -0,0 +1,406 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { TokenizationRegistry } from 'vs/editor/common/languages'; +import { HoverOperation, HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverContext, IEditorHoverParticipant, IHoverPart, IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { HoverVerbosityAction } from 'vs/editor/common/standalone/standaloneEnums'; +import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHoverWidget'; +import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer'; +import { HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; +import { Emitter } from 'vs/base/common/event'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; +import { isMousePositionWithinElement } from 'vs/editor/contrib/hover/browser/hoverUtils'; + +export class ContentHoverController extends Disposable implements IHoverWidget { + + private _currentResult: HoverResult | null = null; + private _renderedContentHover: RenderedContentHover | undefined; + + private readonly _computer: ContentHoverComputer; + private readonly _contentHoverWidget: ContentHoverWidget; + private readonly _participants: IEditorHoverParticipant[]; + private readonly _hoverOperation: HoverOperation; + + private readonly _onContentsChanged = this._register(new Emitter()); + public readonly onContentsChanged = this._onContentsChanged.event; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + super(); + this._contentHoverWidget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); + this._participants = this._initializeHoverParticipants(); + this._computer = new ContentHoverComputer(this._editor, this._participants); + this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer)); + this._registerListeners(); + } + + private _initializeHoverParticipants(): IEditorHoverParticipant[] { + const participants: IEditorHoverParticipant[] = []; + for (const participant of HoverParticipantRegistry.getAll()) { + const participantInstance = this._instantiationService.createInstance(participant, this._editor); + participants.push(participantInstance); + } + participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); + this._register(this._contentHoverWidget.onDidResize(() => { + this._participants.forEach(participant => participant.handleResize?.()); + })); + return participants; + } + + private _registerListeners(): void { + this._register(this._hoverOperation.onResult((result) => { + if (!this._computer.anchor) { + // invalid state, ignore result + return; + } + const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result.value) : result.value); + this._withResult(new HoverResult(this._computer.anchor, messages, result.isComplete)); + })); + const contentHoverWidgetNode = this._contentHoverWidget.getDomNode(); + this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'keydown', (e) => { + if (e.equals(KeyCode.Escape)) { + this.hide(); + } + })); + this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'mouseleave', (e) => { + this._onMouseLeave(e); + })); + this._register(TokenizationRegistry.onDidChange(() => { + if (this._contentHoverWidget.position && this._currentResult) { + this._setCurrentResult(this._currentResult); // render again + } + })); + } + + /** + * Returns true if the hover shows now or will show. + */ + private _startShowingOrUpdateHover( + anchor: HoverAnchor | null, + mode: HoverStartMode, + source: HoverStartSource, + focus: boolean, + mouseEvent: IEditorMouseEvent | null + ): boolean { + const contentHoverIsVisible = this._contentHoverWidget.position && this._currentResult; + if (!contentHoverIsVisible) { + if (anchor) { + this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); + return true; + } + return false; + } + const isHoverSticky = this._editor.getOption(EditorOption.hover).sticky; + const isMouseGettingCloser = mouseEvent && this._contentHoverWidget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy); + const isHoverStickyAndIsMouseGettingCloser = isHoverSticky && isMouseGettingCloser; + // The mouse is getting closer to the hover, so we will keep the hover untouched + // But we will kick off a hover update at the new anchor, insisting on keeping the hover visible. + if (isHoverStickyAndIsMouseGettingCloser) { + if (anchor) { + this._startHoverOperationIfNecessary(anchor, mode, source, focus, true); + } + return true; + } + // If mouse is not getting closer and anchor not defined, hide the hover + if (!anchor) { + this._setCurrentResult(null); + return false; + } + // If mouse if not getting closer and anchor is defined, and the new anchor is the same as the previous anchor + const currentAnchorEqualsPreviousAnchor = this._currentResult!.anchor.equals(anchor); + if (currentAnchorEqualsPreviousAnchor) { + return true; + } + // If mouse if not getting closer and anchor is defined, and the new anchor is not compatible with the previous anchor + const currentAnchorCompatibleWithPreviousAnchor = anchor.canAdoptVisibleHover(this._currentResult!.anchor, this._contentHoverWidget.position); + if (!currentAnchorCompatibleWithPreviousAnchor) { + this._setCurrentResult(null); + this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); + return true; + } + // We aren't getting any closer to the hover, so we will filter existing results + // and keep those which also apply to the new anchor. + this._setCurrentResult(this._currentResult!.filter(anchor)); + this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); + return true; + } + + private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, source: HoverStartSource, focus: boolean, insistOnKeepingHoverVisible: boolean): void { + const currentAnchorEqualToPreviousHover = this._computer.anchor && this._computer.anchor.equals(anchor); + if (currentAnchorEqualToPreviousHover) { + return; + } + this._hoverOperation.cancel(); + this._computer.anchor = anchor; + this._computer.shouldFocus = focus; + this._computer.source = source; + this._computer.insistOnKeepingHoverVisible = insistOnKeepingHoverVisible; + this._hoverOperation.start(mode); + } + + private _setCurrentResult(hoverResult: HoverResult | null): void { + let currentHoverResult = hoverResult; + const currentResultEqualToPreviousResult = this._currentResult === currentHoverResult; + if (currentResultEqualToPreviousResult) { + return; + } + const currentHoverResultIsEmpty = currentHoverResult && currentHoverResult.hoverParts.length === 0; + if (currentHoverResultIsEmpty) { + currentHoverResult = null; + } + this._currentResult = currentHoverResult; + if (this._currentResult) { + this._showHover(this._currentResult); + } else { + this._hideHover(); + } + } + + private _addLoadingMessage(result: IHoverPart[]): IHoverPart[] { + if (!this._computer.anchor) { + return result; + } + for (const participant of this._participants) { + if (!participant.createLoadingMessage) { + continue; + } + const loadingMessage = participant.createLoadingMessage(this._computer.anchor); + if (!loadingMessage) { + continue; + } + return result.slice(0).concat([loadingMessage]); + } + return result; + } + + private _withResult(hoverResult: HoverResult): void { + const previousHoverIsVisibleWithCompleteResult = this._contentHoverWidget.position && this._currentResult && this._currentResult.isComplete; + if (!previousHoverIsVisibleWithCompleteResult) { + this._setCurrentResult(hoverResult); + } + // The hover is visible with a previous complete result. + const isCurrentHoverResultComplete = hoverResult.isComplete; + if (!isCurrentHoverResultComplete) { + // Instead of rendering the new partial result, we wait for the result to be complete. + return; + } + const currentHoverResultIsEmpty = hoverResult.hoverParts.length === 0; + const insistOnKeepingPreviousHoverVisible = this._computer.insistOnKeepingHoverVisible; + const shouldKeepPreviousHoverVisible = currentHoverResultIsEmpty && insistOnKeepingPreviousHoverVisible; + if (shouldKeepPreviousHoverVisible) { + // The hover would now hide normally, so we'll keep the previous messages + return; + } + this._setCurrentResult(hoverResult); + } + + private _showHover(hoverResult: HoverResult): void { + const context = this._getHoverContext(); + this._renderedContentHover = new RenderedContentHover(this._editor, hoverResult, this._participants, this._computer, context, this._keybindingService); + if (this._renderedContentHover.domNodeHasChildren) { + this._contentHoverWidget.show(this._renderedContentHover); + } else { + this._renderedContentHover.dispose(); + } + } + + private _hideHover(): void { + this._contentHoverWidget.hide(); + } + + private _getHoverContext(): IEditorHoverContext { + const hide = () => { + this.hide(); + }; + const onContentsChanged = () => { + this._onContentsChanged.fire(); + this._contentHoverWidget.onContentsChanged(); + }; + const setMinimumDimensions = (dimensions: dom.Dimension) => { + this._contentHoverWidget.setMinimumDimensions(dimensions); + }; + return { hide, onContentsChanged, setMinimumDimensions }; + } + + + public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean { + const isContentWidgetResizing = this._contentHoverWidget.isResizing; + if (isContentWidgetResizing) { + return true; + } + const anchorCandidates: HoverAnchor[] = this._findHoverAnchorCandidates(mouseEvent); + const anchorCandidatesExist = anchorCandidates.length > 0; + if (!anchorCandidatesExist) { + return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); + } + const anchor = anchorCandidates[0]; + return this._startShowingOrUpdateHover(anchor, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); + } + + private _findHoverAnchorCandidates(mouseEvent: IEditorMouseEvent): HoverAnchor[] { + const anchorCandidates: HoverAnchor[] = []; + for (const participant of this._participants) { + if (!participant.suggestHoverAnchor) { + continue; + } + const anchor = participant.suggestHoverAnchor(mouseEvent); + if (!anchor) { + continue; + } + anchorCandidates.push(anchor); + } + const target = mouseEvent.target; + switch (target.type) { + case MouseTargetType.CONTENT_TEXT: { + anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); + break; + } + case MouseTargetType.CONTENT_EMPTY: { + const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; + // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough + const mouseIsWithinLinesAndCloseToHover = !target.detail.isAfterLines + && typeof target.detail.horizontalDistanceToText === 'number' + && target.detail.horizontalDistanceToText < epsilon; + if (!mouseIsWithinLinesAndCloseToHover) { + break; + } + anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); + break; + } + } + anchorCandidates.sort((a, b) => b.priority - a.priority); + return anchorCandidates; + } + + private _onMouseLeave(e: MouseEvent): void { + const editorDomNode = this._editor.getDomNode(); + const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y); + if (isMousePositionOutsideOfEditor) { + this.hide(); + } + } + + public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void { + this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null); + } + + public getWidgetContent(): string | undefined { + const node = this._contentHoverWidget.getDomNode(); + if (!node.textContent) { + return undefined; + } + return node.textContent; + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + this._renderedContentHover?.updateHoverVerbosityLevel(action, index, focus); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedContentHover?.doesHoverAtIndexSupportVerbosityAction(index, action) ?? false; + } + + public getAccessibleWidgetContent(): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContentAtIndex(index); + } + + public focusedHoverPartIndex(): number { + return this._renderedContentHover?.focusedHoverPartIndex ?? -1; + } + + public containsNode(node: Node | null | undefined): boolean { + return (node ? this._contentHoverWidget.getDomNode().contains(node) : false); + } + + public focus(): void { + this._contentHoverWidget.focus(); + } + + public focusHoverPartWithIndex(index: number): void { + this._renderedContentHover?.focusHoverPartWithIndex(index); + } + + public scrollUp(): void { + this._contentHoverWidget.scrollUp(); + } + + public scrollDown(): void { + this._contentHoverWidget.scrollDown(); + } + + public scrollLeft(): void { + this._contentHoverWidget.scrollLeft(); + } + + public scrollRight(): void { + this._contentHoverWidget.scrollRight(); + } + + public pageUp(): void { + this._contentHoverWidget.pageUp(); + } + + public pageDown(): void { + this._contentHoverWidget.pageDown(); + } + + public goToTop(): void { + this._contentHoverWidget.goToTop(); + } + + public goToBottom(): void { + this._contentHoverWidget.goToBottom(); + } + + public hide(): void { + this._computer.anchor = null; + this._hoverOperation.cancel(); + this._setCurrentResult(null); + } + + public getDomNode(): HTMLElement { + return this._contentHoverWidget.getDomNode(); + } + + public get isColorPickerVisible(): boolean { + return this._renderedContentHover?.isColorPickerVisible() ?? false; + } + + public get isVisibleFromKeyboard(): boolean { + return this._contentHoverWidget.isVisibleFromKeyboard; + } + + public get isVisible(): boolean { + return this._contentHoverWidget.isVisible; + } + + public get isFocused(): boolean { + return this._contentHoverWidget.isFocused; + } + + public get isResizing(): boolean { + return this._contentHoverWidget.isResizing; + } + + public get widget() { + return this._contentHoverWidget; + } +} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts new file mode 100644 index 00000000000..9308e6f295d --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -0,0 +1,436 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorHoverContext, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer'; +import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; +import { HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; +import * as dom from 'vs/base/browser/dom'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; +import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; +import { ColorHoverParticipant } from 'vs/editor/contrib/colorPicker/browser/colorHoverParticipant'; +import { localize } from 'vs/nls'; +import { InlayHintsHover } from 'vs/editor/contrib/inlayHints/browser/inlayHintsHover'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; + +export class RenderedContentHover extends Disposable { + + public closestMouseDistance: number | undefined; + public initialMousePosX: number | undefined; + public initialMousePosY: number | undefined; + + public readonly showAtPosition: Position; + public readonly showAtSecondaryPosition: Position; + public readonly shouldFocus: boolean; + public readonly source: HoverStartSource; + public readonly shouldAppearBeforeContent: boolean; + + private readonly _renderedHoverParts: RenderedContentHoverParts; + + constructor( + editor: ICodeEditor, + hoverResult: HoverResult, + participants: IEditorHoverParticipant[], + computer: ContentHoverComputer, + context: IEditorHoverContext, + keybindingService: IKeybindingService + ) { + super(); + const anchor = hoverResult.anchor; + const parts = hoverResult.hoverParts; + this._renderedHoverParts = this._register(new RenderedContentHoverParts( + editor, + participants, + parts, + keybindingService, + context + )); + const { showAtPosition, showAtSecondaryPosition } = RenderedContentHover.computeHoverPositions(editor, anchor.range, parts); + this.shouldAppearBeforeContent = parts.some(m => m.isBeforeContent); + this.showAtPosition = showAtPosition; + this.showAtSecondaryPosition = showAtSecondaryPosition; + this.initialMousePosX = anchor.initialMousePosX; + this.initialMousePosY = anchor.initialMousePosY; + this.shouldFocus = computer.shouldFocus; + this.source = computer.source; + } + + public get domNode(): DocumentFragment { + return this._renderedHoverParts.domNode; + } + + public get domNodeHasChildren(): boolean { + return this._renderedHoverParts.domNodeHasChildren; + } + + public get focusedHoverPartIndex(): number { + return this._renderedHoverParts.focusedHoverPartIndex; + } + + public focusHoverPartWithIndex(index: number): void { + this._renderedHoverParts.focusHoverPartWithIndex(index); + } + + public getAccessibleWidgetContent(): string { + return this._renderedHoverParts.getAccessibleContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string { + return this._renderedHoverParts.getAccessibleHoverContentAtIndex(index); + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + this._renderedHoverParts.updateHoverVerbosityLevel(action, index, focus); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedHoverParts.doesHoverAtIndexSupportVerbosityAction(index, action); + } + + public isColorPickerVisible(): boolean { + return this._renderedHoverParts.isColorPickerVisible(); + } + + public static computeHoverPositions(editor: ICodeEditor, anchorRange: Range, hoverParts: IHoverPart[]): { showAtPosition: Position; showAtSecondaryPosition: Position } { + + let startColumnBoundary = 1; + if (editor.hasModel()) { + // Ensure the range is on the current view line + const viewModel = editor._getViewModel(); + const coordinatesConverter = viewModel.coordinatesConverter; + const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange); + const anchorViewMinColumn = viewModel.getLineMinColumn(anchorViewRange.startLineNumber); + const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, anchorViewMinColumn); + startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column; + } + + // The anchor range is always on a single line + const anchorStartLineNumber = anchorRange.startLineNumber; + let secondaryPositionColumn = anchorRange.startColumn; + let forceShowAtRange: Range | undefined; + + for (const hoverPart of hoverParts) { + const hoverPartRange = hoverPart.range; + const hoverPartRangeOnAnchorStartLine = hoverPartRange.startLineNumber === anchorStartLineNumber; + const hoverPartRangeOnAnchorEndLine = hoverPartRange.endLineNumber === anchorStartLineNumber; + const hoverPartRangeIsOnAnchorLine = hoverPartRangeOnAnchorStartLine && hoverPartRangeOnAnchorEndLine; + if (hoverPartRangeIsOnAnchorLine) { + // this message has a range that is completely sitting on the line of the anchor + const hoverPartStartColumn = hoverPartRange.startColumn; + const minSecondaryPositionColumn = Math.min(secondaryPositionColumn, hoverPartStartColumn); + secondaryPositionColumn = Math.max(minSecondaryPositionColumn, startColumnBoundary); + } + if (hoverPart.forceShowAtRange) { + forceShowAtRange = hoverPartRange; + } + } + + let showAtPosition: Position; + let showAtSecondaryPosition: Position; + if (forceShowAtRange) { + const forceShowAtPosition = forceShowAtRange.getStartPosition(); + showAtPosition = forceShowAtPosition; + showAtSecondaryPosition = forceShowAtPosition; + } else { + showAtPosition = anchorRange.getStartPosition(); + showAtSecondaryPosition = new Position(anchorStartLineNumber, secondaryPositionColumn); + } + return { + showAtPosition, + showAtSecondaryPosition, + }; + } +} + +interface IRenderedContentHoverPart { + /** + * Type of rendered part + */ + type: 'hoverPart'; + /** + * Participant of the rendered hover part + */ + participant: IEditorHoverParticipant; + /** + * The rendered hover part + */ + hoverPart: IHoverPart; + /** + * The HTML element containing the hover status bar. + */ + hoverElement: HTMLElement; +} + +interface IRenderedContentStatusBar { + /** + * Type of rendered part + */ + type: 'statusBar'; + /** + * The HTML element containing the hover status bar. + */ + hoverElement: HTMLElement; + /** + * The actions of the hover status bar. + */ + actions: HoverAction[]; +} + +type IRenderedContentHoverPartOrStatusBar = IRenderedContentHoverPart | IRenderedContentStatusBar; + +class RenderedStatusBar implements IDisposable { + + constructor(fragment: DocumentFragment, private readonly _statusBar: EditorHoverStatusBar) { + fragment.appendChild(this._statusBar.hoverElement); + } + + get hoverElement(): HTMLElement { + return this._statusBar.hoverElement; + } + + get actions(): HoverAction[] { + return this._statusBar.actions; + } + + dispose() { + this._statusBar.dispose(); + } +} + +class RenderedContentHoverParts extends Disposable { + + private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'content-hover-highlight', + className: 'hoverHighlight' + }); + + private readonly _renderedParts: IRenderedContentHoverPartOrStatusBar[] = []; + private readonly _fragment: DocumentFragment; + private readonly _context: IEditorHoverContext; + + private _markdownHoverParticipant: MarkdownHoverParticipant | undefined; + private _colorHoverParticipant: ColorHoverParticipant | undefined; + private _focusedHoverPartIndex: number = -1; + + constructor( + editor: ICodeEditor, + participants: IEditorHoverParticipant[], + hoverParts: IHoverPart[], + keybindingService: IKeybindingService, + context: IEditorHoverContext + ) { + super(); + this._context = context; + this._fragment = document.createDocumentFragment(); + this._register(this._renderParts(participants, hoverParts, context, keybindingService)); + this._register(this._registerListenersOnRenderedParts()); + this._register(this._createEditorDecorations(editor, hoverParts)); + this._updateMarkdownAndColorParticipantInfo(participants); + } + + private _createEditorDecorations(editor: ICodeEditor, hoverParts: IHoverPart[]): IDisposable { + if (hoverParts.length === 0) { + return Disposable.None; + } + let highlightRange = hoverParts[0].range; + for (const hoverPart of hoverParts) { + const hoverPartRange = hoverPart.range; + highlightRange = Range.plusRange(highlightRange, hoverPartRange); + } + const highlightDecoration = editor.createDecorationsCollection(); + highlightDecoration.set([{ + range: highlightRange, + options: RenderedContentHoverParts._DECORATION_OPTIONS + }]); + return toDisposable(() => { + highlightDecoration.clear(); + }); + } + + private _renderParts(participants: IEditorHoverParticipant[], hoverParts: IHoverPart[], hoverContext: IEditorHoverContext, keybindingService: IKeybindingService): IDisposable { + const statusBar = new EditorHoverStatusBar(keybindingService); + const hoverRenderingContext: IEditorHoverRenderContext = { + fragment: this._fragment, + statusBar, + ...hoverContext + }; + const disposables = new DisposableStore(); + for (const participant of participants) { + const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext); + disposables.add(renderedHoverParts); + for (const renderedHoverPart of renderedHoverParts.renderedHoverParts) { + this._renderedParts.push({ + type: 'hoverPart', + participant, + hoverPart: renderedHoverPart.hoverPart, + hoverElement: renderedHoverPart.hoverElement, + }); + } + } + const renderedStatusBar = this._renderStatusBar(this._fragment, statusBar); + if (renderedStatusBar) { + disposables.add(renderedStatusBar); + this._renderedParts.push({ + type: 'statusBar', + hoverElement: renderedStatusBar.hoverElement, + actions: renderedStatusBar.actions, + }); + } + return toDisposable(() => { disposables.dispose(); }); + } + + private _renderHoverPartsForParticipant(hoverParts: IHoverPart[], participant: IEditorHoverParticipant, hoverRenderingContext: IEditorHoverRenderContext): IRenderedHoverParts { + const hoverPartsForParticipant = hoverParts.filter(hoverPart => hoverPart.owner === participant); + const hasHoverPartsForParticipant = hoverPartsForParticipant.length > 0; + if (!hasHoverPartsForParticipant) { + return new RenderedHoverParts([]); + } + return participant.renderHoverParts(hoverRenderingContext, hoverPartsForParticipant); + } + + private _renderStatusBar(fragment: DocumentFragment, statusBar: EditorHoverStatusBar): RenderedStatusBar | undefined { + if (!statusBar.hasContent) { + return undefined; + } + return new RenderedStatusBar(fragment, statusBar); + } + + private _registerListenersOnRenderedParts(): IDisposable { + const disposables = new DisposableStore(); + this._renderedParts.forEach((renderedPart: IRenderedContentHoverPartOrStatusBar, index: number) => { + const element = renderedPart.hoverElement; + element.tabIndex = 0; + disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = index; + })); + disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = -1; + })); + }); + return disposables; + } + + private _updateMarkdownAndColorParticipantInfo(participants: IEditorHoverParticipant[]) { + const markdownHoverParticipant = participants.find(p => { + return (p instanceof MarkdownHoverParticipant) && !(p instanceof InlayHintsHover); + }); + if (markdownHoverParticipant) { + this._markdownHoverParticipant = markdownHoverParticipant as MarkdownHoverParticipant; + } + this._colorHoverParticipant = participants.find(p => p instanceof ColorHoverParticipant); + } + + public focusHoverPartWithIndex(index: number): void { + if (index < 0 || index >= this._renderedParts.length) { + return; + } + this._renderedParts[index].hoverElement.focus(); + } + + public getAccessibleContent(): string { + const content: string[] = []; + for (let i = 0; i < this._renderedParts.length; i++) { + content.push(this.getAccessibleHoverContentAtIndex(i)); + } + return content.join('\n\n'); + } + + public getAccessibleHoverContentAtIndex(index: number): string { + const renderedPart = this._renderedParts[index]; + if (!renderedPart) { + return ''; + } + if (renderedPart.type === 'statusBar') { + const statusBarDescription = [localize('hoverAccessibilityStatusBar', "This is a hover status bar.")]; + for (const action of renderedPart.actions) { + const keybinding = action.actionKeybindingLabel; + if (keybinding) { + statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithKeybinding', "It has an action with label {0} and keybinding {1}.", action.actionLabel, keybinding)); + } else { + statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithoutKeybinding', "It has an action with label {0}.", action.actionLabel)); + } + } + return statusBarDescription.join('\n'); + } + return renderedPart.participant.getAccessibleContent(renderedPart.hoverPart); + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + if (!this._markdownHoverParticipant) { + return; + } + const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index); + if (normalizedMarkdownHoverIndex === undefined) { + return; + } + const renderedPart = await this._markdownHoverParticipant.updateMarkdownHoverVerbosityLevel(action, normalizedMarkdownHoverIndex, focus); + if (!renderedPart) { + return; + } + this._renderedParts[index] = { + type: 'hoverPart', + participant: this._markdownHoverParticipant, + hoverPart: renderedPart.hoverPart, + hoverElement: renderedPart.hoverElement, + }; + this._context.onContentsChanged(); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + if (!this._markdownHoverParticipant) { + return false; + } + const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index); + if (normalizedMarkdownHoverIndex === undefined) { + return false; + } + return this._markdownHoverParticipant.doesMarkdownHoverAtIndexSupportVerbosityAction(normalizedMarkdownHoverIndex, action); + } + + public isColorPickerVisible(): boolean { + return this._colorHoverParticipant?.isColorPickerVisible() ?? false; + } + + private _normalizedIndexToMarkdownHoverIndexRange(markdownHoverParticipant: MarkdownHoverParticipant, index: number): number | undefined { + const renderedPart = this._renderedParts[index]; + if (!renderedPart || renderedPart.type !== 'hoverPart') { + return undefined; + } + const isHoverPartMarkdownHover = renderedPart.participant === markdownHoverParticipant; + if (!isHoverPartMarkdownHover) { + return undefined; + } + const firstIndexOfMarkdownHovers = this._renderedParts.findIndex(renderedPart => + renderedPart.type === 'hoverPart' + && renderedPart.participant === markdownHoverParticipant + ); + if (firstIndexOfMarkdownHovers === -1) { + throw new BugIndicatingError(); + } + return index - firstIndexOfMarkdownHovers; + } + + public get domNode(): DocumentFragment { + return this._fragment; + } + + public get domNodeHasChildren(): boolean { + return this._fragment.hasChildNodes(); + } + + public get focusedHoverPartIndex(): number { + return this._focusedHoverPartIndex; + } +} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts new file mode 100644 index 00000000000..bb43959419b --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as dom from 'vs/base/browser/dom'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IEditorHoverAction, IEditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +const $ = dom.$; + +export class EditorHoverStatusBar extends Disposable implements IEditorHoverStatusBar { + + public readonly hoverElement: HTMLElement; + public readonly actions: HoverAction[] = []; + + private readonly actionsElement: HTMLElement; + private _hasContent: boolean = false; + + public get hasContent() { + return this._hasContent; + } + + constructor( + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + super(); + this.hoverElement = $('div.hover-row.status-bar'); + this.hoverElement.tabIndex = 0; + this.actionsElement = dom.append(this.hoverElement, $('div.actions')); + } + + public addAction( + actionOptions: { + label: string; + iconClass?: string; run: (target: HTMLElement) => void; + commandId: string; + }): IEditorHoverAction { + + const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + this._hasContent = true; + const action = this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel)); + this.actions.push(action); + return action; + } + + public append(element: HTMLElement): HTMLElement { + const result = dom.append(this.actionsElement, element); + this._hasContent = true; + return result; + } +} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts b/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts new file mode 100644 index 00000000000..a52d758baa7 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { HoverAnchor, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; + +export class HoverResult { + + constructor( + public readonly anchor: HoverAnchor, + public readonly hoverParts: IHoverPart[], + public readonly isComplete: boolean + ) { } + + public filter(anchor: HoverAnchor): HoverResult { + const filteredHoverParts = this.hoverParts.filter((m) => m.isValidForHoverAnchor(anchor)); + if (filteredHoverParts.length === this.hoverParts.length) { + return this; + } + return new FilteredHoverResult(this, this.anchor, filteredHoverParts, this.isComplete); + } +} + +export class FilteredHoverResult extends HoverResult { + + constructor( + private readonly original: HoverResult, + anchor: HoverAnchor, + messages: IHoverPart[], + isComplete: boolean + ) { + super(anchor, messages, isComplete); + } + + public override filter(anchor: HoverAnchor): HoverResult { + return this.original.filter(anchor); + } +} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts new file mode 100644 index 00000000000..37cf8740e37 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -0,0 +1,466 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ResizableContentWidget } from 'vs/editor/contrib/hover/browser/resizableContentWidget'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { getHoverAccessibleViewHint, HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; +import { PositionAffinity } from 'vs/editor/common/model'; +import { Emitter } from 'vs/base/common/event'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; + +const HORIZONTAL_SCROLLING_BY = 30; +const CONTAINER_HEIGHT_PADDING = 6; + +export class ContentHoverWidget extends ResizableContentWidget { + + public static ID = 'editor.contrib.resizableContentHoverWidget'; + private static _lastDimensions: dom.Dimension = new dom.Dimension(0, 0); + + private _renderedHover: RenderedContentHover | undefined; + private _positionPreference: ContentWidgetPositionPreference | undefined; + private _minimumSize: dom.Dimension; + private _contentWidth: number | undefined; + + private readonly _hover: HoverWidget = this._register(new HoverWidget()); + private readonly _hoverVisibleKey: IContextKey; + private readonly _hoverFocusedKey: IContextKey; + + private readonly _onDidResize = this._register(new Emitter()); + public readonly onDidResize = this._onDidResize.event; + + public get isVisibleFromKeyboard(): boolean { + return (this._renderedHover?.source === HoverStartSource.Keyboard); + } + + public get isVisible(): boolean { + return this._hoverVisibleKey.get() ?? false; + } + + public get isFocused(): boolean { + return this._hoverFocusedKey.get() ?? false; + } + + constructor( + editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IKeybindingService private readonly _keybindingService: IKeybindingService + ) { + const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8; + const minimumWidth = 150; + const minimumSize = new dom.Dimension(minimumWidth, minimumHeight); + super(editor, minimumSize); + + this._minimumSize = minimumSize; + this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(contextKeyService); + this._hoverFocusedKey = EditorContextKeys.hoverFocused.bindTo(contextKeyService); + + dom.append(this._resizableNode.domNode, this._hover.containerDomNode); + this._resizableNode.domNode.style.zIndex = '50'; + + this._register(this._editor.onDidLayoutChange(() => { + if (this.isVisible) { + this._updateMaxDimensions(); + } + })); + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (e.hasChanged(EditorOption.fontInfo)) { + this._updateFont(); + } + })); + const focusTracker = this._register(dom.trackFocus(this._resizableNode.domNode)); + this._register(focusTracker.onDidFocus(() => { + this._hoverFocusedKey.set(true); + })); + this._register(focusTracker.onDidBlur(() => { + this._hoverFocusedKey.set(false); + })); + this._setRenderedHover(undefined); + this._editor.addContentWidget(this); + } + + public override dispose(): void { + super.dispose(); + this._renderedHover?.dispose(); + this._editor.removeContentWidget(this); + } + + public getId(): string { + return ContentHoverWidget.ID; + } + + private static _applyDimensions(container: HTMLElement, width: number | string, height: number | string): void { + const transformedWidth = typeof width === 'number' ? `${width}px` : width; + const transformedHeight = typeof height === 'number' ? `${height}px` : height; + container.style.width = transformedWidth; + container.style.height = transformedHeight; + } + + private _setContentsDomNodeDimensions(width: number | string, height: number | string): void { + const contentsDomNode = this._hover.contentsDomNode; + return ContentHoverWidget._applyDimensions(contentsDomNode, width, height); + } + + private _setContainerDomNodeDimensions(width: number | string, height: number | string): void { + const containerDomNode = this._hover.containerDomNode; + return ContentHoverWidget._applyDimensions(containerDomNode, width, height); + } + + private _setHoverWidgetDimensions(width: number | string, height: number | string): void { + this._setContentsDomNodeDimensions(width, height); + this._setContainerDomNodeDimensions(width, height); + this._layoutContentWidget(); + } + + private static _applyMaxDimensions(container: HTMLElement, width: number | string, height: number | string) { + const transformedWidth = typeof width === 'number' ? `${width}px` : width; + const transformedHeight = typeof height === 'number' ? `${height}px` : height; + container.style.maxWidth = transformedWidth; + container.style.maxHeight = transformedHeight; + } + + private _setHoverWidgetMaxDimensions(width: number | string, height: number | string): void { + ContentHoverWidget._applyMaxDimensions(this._hover.contentsDomNode, width, height); + ContentHoverWidget._applyMaxDimensions(this._hover.containerDomNode, width, height); + this._hover.containerDomNode.style.setProperty('--vscode-hover-maxWidth', typeof width === 'number' ? `${width}px` : width); + this._layoutContentWidget(); + } + + private _setAdjustedHoverWidgetDimensions(size: dom.Dimension): void { + this._setHoverWidgetMaxDimensions('none', 'none'); + const width = size.width; + const height = size.height; + this._setHoverWidgetDimensions(width, height); + } + + private _updateResizableNodeMaxDimensions(): void { + const maxRenderingWidth = this._findMaximumRenderingWidth() ?? Infinity; + const maxRenderingHeight = this._findMaximumRenderingHeight() ?? Infinity; + this._resizableNode.maxSize = new dom.Dimension(maxRenderingWidth, maxRenderingHeight); + this._setHoverWidgetMaxDimensions(maxRenderingWidth, maxRenderingHeight); + } + + protected override _resize(size: dom.Dimension): void { + ContentHoverWidget._lastDimensions = new dom.Dimension(size.width, size.height); + this._setAdjustedHoverWidgetDimensions(size); + this._resizableNode.layout(size.height, size.width); + this._updateResizableNodeMaxDimensions(); + this._hover.scrollbar.scanDomNode(); + this._editor.layoutContentWidget(this); + this._onDidResize.fire(); + } + + private _findAvailableSpaceVertically(): number | undefined { + const position = this._renderedHover?.showAtPosition; + if (!position) { + return; + } + return this._positionPreference === ContentWidgetPositionPreference.ABOVE ? + this._availableVerticalSpaceAbove(position) + : this._availableVerticalSpaceBelow(position); + } + + private _findMaximumRenderingHeight(): number | undefined { + const availableSpace = this._findAvailableSpaceVertically(); + if (!availableSpace) { + return; + } + // Padding needed in order to stop the resizing down to a smaller height + let maximumHeight = CONTAINER_HEIGHT_PADDING; + Array.from(this._hover.contentsDomNode.children).forEach((hoverPart) => { + maximumHeight += hoverPart.clientHeight; + }); + + return Math.min(availableSpace, maximumHeight); + } + + private _isHoverTextOverflowing(): boolean { + // To find out if the text is overflowing, we will disable wrapping, check the widths, and then re-enable wrapping + this._hover.containerDomNode.style.setProperty('--vscode-hover-whiteSpace', 'nowrap'); + this._hover.containerDomNode.style.setProperty('--vscode-hover-sourceWhiteSpace', 'nowrap'); + + const overflowing = Array.from(this._hover.contentsDomNode.children).some((hoverElement) => { + return hoverElement.scrollWidth > hoverElement.clientWidth; + }); + + this._hover.containerDomNode.style.removeProperty('--vscode-hover-whiteSpace'); + this._hover.containerDomNode.style.removeProperty('--vscode-hover-sourceWhiteSpace'); + + return overflowing; + } + + private _findMaximumRenderingWidth(): number | undefined { + if (!this._editor || !this._editor.hasModel()) { + return; + } + + const overflowing = this._isHoverTextOverflowing(); + const initialWidth = ( + typeof this._contentWidth === 'undefined' + ? 0 + : this._contentWidth - 2 // - 2 for the borders + ); + + if (overflowing || this._hover.containerDomNode.clientWidth < initialWidth) { + const bodyBoxWidth = dom.getClientArea(this._hover.containerDomNode.ownerDocument.body).width; + const horizontalPadding = 14; + return bodyBoxWidth - horizontalPadding; + } else { + return this._hover.containerDomNode.clientWidth + 2; + } + } + + public isMouseGettingCloser(posx: number, posy: number): boolean { + + if (!this._renderedHover) { + return false; + } + if (this._renderedHover.initialMousePosX === undefined || this._renderedHover.initialMousePosY === undefined) { + this._renderedHover.initialMousePosX = posx; + this._renderedHover.initialMousePosY = posy; + return false; + } + + const widgetRect = dom.getDomNodePagePosition(this.getDomNode()); + if (this._renderedHover.closestMouseDistance === undefined) { + this._renderedHover.closestMouseDistance = computeDistanceFromPointToRectangle( + this._renderedHover.initialMousePosX, + this._renderedHover.initialMousePosY, + widgetRect.left, + widgetRect.top, + widgetRect.width, + widgetRect.height + ); + } + + const distance = computeDistanceFromPointToRectangle( + posx, + posy, + widgetRect.left, + widgetRect.top, + widgetRect.width, + widgetRect.height + ); + if (distance > this._renderedHover.closestMouseDistance + 4 /* tolerance of 4 pixels */) { + // The mouse is getting farther away + return false; + } + + this._renderedHover.closestMouseDistance = Math.min(this._renderedHover.closestMouseDistance, distance); + return true; + } + + private _setRenderedHover(renderedHover: RenderedContentHover | undefined): void { + this._renderedHover?.dispose(); + this._renderedHover = renderedHover; + this._hoverVisibleKey.set(!!renderedHover); + this._hover.containerDomNode.classList.toggle('hidden', !renderedHover); + } + + private _updateFont(): void { + const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); + const contentsDomNode = this._hover.contentsDomNode; + contentsDomNode.style.fontSize = `${fontSize}px`; + contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`; + const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); + codeClasses.forEach(node => this._editor.applyFontInfo(node)); + } + + private _updateContent(node: DocumentFragment): void { + const contentsDomNode = this._hover.contentsDomNode; + contentsDomNode.style.paddingBottom = ''; + contentsDomNode.textContent = ''; + contentsDomNode.appendChild(node); + } + + private _layoutContentWidget(): void { + this._editor.layoutContentWidget(this); + this._hover.onContentsChanged(); + } + + private _updateMaxDimensions() { + const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height); + const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 500, ContentHoverWidget._lastDimensions.width); + this._setHoverWidgetMaxDimensions(width, height); + } + + private _render(renderedHover: RenderedContentHover) { + this._setRenderedHover(renderedHover); + this._updateFont(); + this._updateContent(renderedHover.domNode); + this._updateMaxDimensions(); + this.onContentsChanged(); + // Simply force a synchronous render on the editor + // such that the widget does not really render with left = '0px' + this._editor.render(); + } + + override getPosition(): IContentWidgetPosition | null { + if (!this._renderedHover) { + return null; + } + return { + position: this._renderedHover.showAtPosition, + secondaryPosition: this._renderedHover.showAtSecondaryPosition, + positionAffinity: this._renderedHover.shouldAppearBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined, + preference: [this._positionPreference ?? ContentWidgetPositionPreference.ABOVE] + }; + } + + public show(renderedHover: RenderedContentHover): void { + if (!this._editor || !this._editor.hasModel()) { + return; + } + this._render(renderedHover); + const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); + const widgetPosition = renderedHover.showAtPosition; + this._positionPreference = this._findPositionPreference(widgetHeight, widgetPosition) ?? ContentWidgetPositionPreference.ABOVE; + + // See https://github.com/microsoft/vscode/issues/140339 + // TODO: Doing a second layout of the hover after force rendering the editor + this.onContentsChanged(); + if (renderedHover.shouldFocus) { + this._hover.containerDomNode.focus(); + } + this._onDidResize.fire(); + // The aria label overrides the label, so if we add to it, add the contents of the hover + const hoverFocused = this._hover.containerDomNode.ownerDocument.activeElement === this._hover.containerDomNode; + const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint( + this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), + this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel() ?? '' + ); + + if (accessibleViewHint) { + this._hover.contentsDomNode.ariaLabel = this._hover.contentsDomNode.textContent + ', ' + accessibleViewHint; + } + } + + public hide(): void { + if (!this._renderedHover) { + return; + } + const hoverStoleFocus = this._renderedHover.shouldFocus || this._hoverFocusedKey.get(); + this._setRenderedHover(undefined); + this._resizableNode.maxSize = new dom.Dimension(Infinity, Infinity); + this._resizableNode.clearSashHoverState(); + this._hoverFocusedKey.set(false); + this._editor.layoutContentWidget(this); + if (hoverStoleFocus) { + this._editor.focus(); + } + } + + private _removeConstraintsRenderNormally(): void { + // Added because otherwise the initial size of the hover content is smaller than should be + const layoutInfo = this._editor.getLayoutInfo(); + this._resizableNode.layout(layoutInfo.height, layoutInfo.width); + this._setHoverWidgetDimensions('auto', 'auto'); + } + + public setMinimumDimensions(dimensions: dom.Dimension): void { + // We combine the new minimum dimensions with the previous ones + this._minimumSize = new dom.Dimension( + Math.max(this._minimumSize.width, dimensions.width), + Math.max(this._minimumSize.height, dimensions.height) + ); + this._updateMinimumWidth(); + } + + private _updateMinimumWidth(): void { + const width = ( + typeof this._contentWidth === 'undefined' + ? this._minimumSize.width + : Math.min(this._contentWidth, this._minimumSize.width) + ); + // We want to avoid that the hover is artificially large, so we use the content width as minimum width + this._resizableNode.minSize = new dom.Dimension(width, this._minimumSize.height); + } + + public onContentsChanged(): void { + this._removeConstraintsRenderNormally(); + const containerDomNode = this._hover.containerDomNode; + + let height = dom.getTotalHeight(containerDomNode); + let width = dom.getTotalWidth(containerDomNode); + this._resizableNode.layout(height, width); + + this._setHoverWidgetDimensions(width, height); + + height = dom.getTotalHeight(containerDomNode); + width = dom.getTotalWidth(containerDomNode); + this._contentWidth = width; + this._updateMinimumWidth(); + this._resizableNode.layout(height, width); + + if (this._renderedHover?.showAtPosition) { + const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); + this._positionPreference = this._findPositionPreference(widgetHeight, this._renderedHover.showAtPosition); + } + this._layoutContentWidget(); + } + + public focus(): void { + this._hover.containerDomNode.focus(); + } + + public scrollUp(): void { + const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; + const fontInfo = this._editor.getOption(EditorOption.fontInfo); + this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - fontInfo.lineHeight }); + } + + public scrollDown(): void { + const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; + const fontInfo = this._editor.getOption(EditorOption.fontInfo); + this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + fontInfo.lineHeight }); + } + + public scrollLeft(): void { + const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft; + this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft - HORIZONTAL_SCROLLING_BY }); + } + + public scrollRight(): void { + const scrollLeft = this._hover.scrollbar.getScrollPosition().scrollLeft; + this._hover.scrollbar.setScrollPosition({ scrollLeft: scrollLeft + HORIZONTAL_SCROLLING_BY }); + } + + public pageUp(): void { + const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; + const scrollHeight = this._hover.scrollbar.getScrollDimensions().height; + this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop - scrollHeight }); + } + + public pageDown(): void { + const scrollTop = this._hover.scrollbar.getScrollPosition().scrollTop; + const scrollHeight = this._hover.scrollbar.getScrollDimensions().height; + this._hover.scrollbar.setScrollPosition({ scrollTop: scrollTop + scrollHeight }); + } + + public goToTop(): void { + this._hover.scrollbar.setScrollPosition({ scrollTop: 0 }); + } + + public goToBottom(): void { + this._hover.scrollbar.setScrollPosition({ scrollTop: this._hover.scrollbar.getScrollDimensions().scrollHeight }); + } +} + +function computeDistanceFromPointToRectangle(pointX: number, pointY: number, left: number, top: number, width: number, height: number): number { + const x = (left + width / 2); // x center of rectangle + const y = (top + height / 2); // y center of rectangle + const dx = Math.max(Math.abs(pointX - x) - width / 2, 0); + const dy = Math.max(Math.abs(pointY - y) - height / 2, 0); + return Math.sqrt(dx * dx + dy * dy); +} diff --git a/src/vs/editor/contrib/hover/browser/getHover.ts b/src/vs/editor/contrib/hover/browser/getHover.ts index a89e872eaba..9fc1d78fb01 100644 --- a/src/vs/editor/contrib/hover/browser/getHover.ts +++ b/src/vs/editor/contrib/hover/browser/getHover.ts @@ -21,31 +21,37 @@ export class HoverProviderResult { ) { } } +/** + * Does not throw or return a rejected promise (returns undefined instead). + */ async function executeProvider(provider: HoverProvider, ordinal: number, model: ITextModel, position: Position, token: CancellationToken): Promise { - try { - const result = await Promise.resolve(provider.provideHover(model, position, token)); - if (result && isValid(result)) { - return new HoverProviderResult(provider, result, ordinal); - } - } catch (err) { - onUnexpectedExternalError(err); + const result = await Promise + .resolve(provider.provideHover(model, position, token)) + .catch(onUnexpectedExternalError); + if (!result || !isValid(result)) { + return undefined; } - return undefined; + return new HoverProviderResult(provider, result, ordinal); } -export function getHover(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): AsyncIterableObject { - const providers = registry.ordered(model); +export function getHoverProviderResultsAsAsyncIterable(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken, recursive = false): AsyncIterableObject { + const providers = registry.ordered(model, recursive); const promises = providers.map((provider, index) => executeProvider(provider, index, model, position, token)); return AsyncIterableObject.fromPromises(promises).coalesce(); } -export function getHoverPromise(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getHover(registry, model, position, token).map(item => item.hover).toPromise(); +export function getHoversPromise(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken, recursive = false): Promise { + return getHoverProviderResultsAsAsyncIterable(registry, model, position, token, recursive).map(item => item.hover).toPromise(); } -registerModelAndPositionCommand('_executeHoverProvider', (accessor, model, position) => { +registerModelAndPositionCommand('_executeHoverProvider', (accessor, model, position): Promise => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - return getHoverPromise(languageFeaturesService.hoverProvider, model, position, CancellationToken.None); + return getHoversPromise(languageFeaturesService.hoverProvider, model, position, CancellationToken.None); +}); + +registerModelAndPositionCommand('_executeHoverProvider_recursive', (accessor, model, position): Promise => { + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + return getHoversPromise(languageFeaturesService.hoverProvider, model, position, CancellationToken.None, true); }); function isValid(result: Hover) { diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 34695750fd4..2520856e59d 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -7,6 +7,12 @@ background-color: var(--vscode-editor-hoverHighlightBackground); } +.monaco-editor .monaco-hover-content { + padding-right: 2px; + padding-bottom: 2px; + box-sizing: border-box; +} + .monaco-editor .monaco-hover { color: var(--vscode-editorHoverWidget-foreground); background-color: var(--vscode-editorHoverWidget-background); @@ -22,6 +28,38 @@ color: var(--vscode-textLink-activeForeground); } +.monaco-editor .monaco-hover .hover-row { + display: flex; +} + +.monaco-editor .monaco-hover .hover-row .hover-row-contents { + min-width:0; + display: flex; + flex-direction: column; +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions { + display: flex; + flex-direction: column; + padding-left: 5px; + padding-right: 5px; + justify-content: end; + border-right: 1px solid var(--vscode-editorHoverWidget-border); +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon { + cursor: pointer; + font-size: 11px; +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.enabled { + color: var(--vscode-textLink-foreground); +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.disabled { + opacity: 0.6; +} + .monaco-editor .monaco-hover .hover-row .actions { background-color: var(--vscode-editorHoverWidget-statusBarBackground); } diff --git a/src/vs/editor/contrib/hover/browser/hover.ts b/src/vs/editor/contrib/hover/browser/hover.ts deleted file mode 100644 index 509eb67e324..00000000000 --- a/src/vs/editor/contrib/hover/browser/hover.ts +++ /dev/null @@ -1,850 +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 { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Range } from 'vs/editor/common/core/range'; -import { IEditorContribution, IScrollEvent } from 'vs/editor/common/editorCommon'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; -import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; -import { ContentHoverWidget, ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHover'; -import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHover'; -import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; -import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; -import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHoverParticipant'; -import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import * as nls from 'vs/nls'; -import 'vs/css!./hover'; - -// sticky hover widget which doesn't disappear on focus out and such -const _sticky = false - // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this - ; - -interface IHoverSettings { - readonly enabled: boolean; - readonly sticky: boolean; - readonly hidingDelay: number; -} - -interface IHoverState { - mouseDown: boolean; - // TODO @aiday-mar maybe not needed, investigate this - contentHoverFocused: boolean; - activatedByDecoratorClick: boolean; -} - -export class HoverController extends Disposable implements IEditorContribution { - - public static readonly ID = 'editor.contrib.hover'; - - private readonly _listenersStore = new DisposableStore(); - - private _glyphWidget: MarginHoverWidget | undefined; - private _contentWidget: ContentHoverController | undefined; - - private _mouseMoveEvent: IEditorMouseEvent | undefined; - private _reactToEditorMouseMoveRunner: RunOnceScheduler; - - private _hoverSettings!: IHoverSettings; - private _hoverState: IHoverState = { - mouseDown: false, - contentHoverFocused: false, - activatedByDecoratorClick: false - }; - - constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IOpenerService private readonly _openerService: IOpenerService, - @ILanguageService private readonly _languageService: ILanguageService, - @IKeybindingService private readonly _keybindingService: IKeybindingService - ) { - super(); - this._reactToEditorMouseMoveRunner = this._register( - new RunOnceScheduler( - () => this._reactToEditorMouseMove(this._mouseMoveEvent), 0 - ) - ); - this._hookListeners(); - this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { - if (e.hasChanged(EditorOption.hover)) { - this._unhookListeners(); - this._hookListeners(); - } - })); - } - - static get(editor: ICodeEditor): HoverController | null { - return editor.getContribution(HoverController.ID); - } - - private _hookListeners(): void { - - const hoverOpts = this._editor.getOption(EditorOption.hover); - this._hoverSettings = { - enabled: hoverOpts.enabled, - sticky: hoverOpts.sticky, - hidingDelay: hoverOpts.delay - }; - - if (hoverOpts.enabled) { - this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); - this._listenersStore.add(this._editor.onMouseUp(() => this._onEditorMouseUp())); - this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); - this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); - } else { - this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); - this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); - } - - this._listenersStore.add(this._editor.onMouseLeave((e) => this._onEditorMouseLeave(e))); - this._listenersStore.add(this._editor.onDidChangeModel(() => { - this._cancelScheduler(); - this._hideWidgets(); - })); - this._listenersStore.add(this._editor.onDidChangeModelContent(() => this._cancelScheduler())); - this._listenersStore.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e))); - } - - private _unhookListeners(): void { - this._listenersStore.clear(); - } - - private _cancelScheduler() { - this._mouseMoveEvent = undefined; - this._reactToEditorMouseMoveRunner.cancel(); - } - - private _onEditorScrollChanged(e: IScrollEvent): void { - if (e.scrollTopChanged || e.scrollLeftChanged) { - this._hideWidgets(); - } - } - - private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { - - this._hoverState.mouseDown = true; - const target = mouseEvent.target; - - if (target.type === MouseTargetType.CONTENT_WIDGET && target.detail === ContentHoverWidget.ID) { - // mouse down on top of content hover widget - this._hoverState.contentHoverFocused = true; - return; - } - - if (target.type === MouseTargetType.OVERLAY_WIDGET && target.detail === MarginHoverWidget.ID) { - // mouse down on top of margin hover widget - return; - } - - if (target.type !== MouseTargetType.OVERLAY_WIDGET) { - this._hoverState.contentHoverFocused = false; - } - - if (this._contentWidget?.widget.isResizing) { - return; - } - - this._hideWidgets(); - } - - private _onEditorMouseUp(): void { - this._hoverState.mouseDown = false; - } - - private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void { - - this._cancelScheduler(); - const targetElement = (mouseEvent.event.browserEvent.relatedTarget) as HTMLElement; - - if (this._contentWidget?.widget.isResizing || this._contentWidget?.containsNode(targetElement)) { - // When the content widget is resizing - // When the mouse is inside hover widget - return; - } - if (_sticky) { - return; - } - this._hideWidgets(); - } - - private _isMouseOverWidget(mouseEvent: IEditorMouseEvent): boolean { - - const target = mouseEvent.target; - const sticky = this._hoverSettings.sticky; - if ( - sticky - && target.type === MouseTargetType.CONTENT_WIDGET - && target.detail === ContentHoverWidget.ID - ) { - // mouse moved on top of content hover widget - return true; - } - if ( - sticky - && this._contentWidget?.containsNode(mouseEvent.event.browserEvent.view?.document.activeElement) - && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed - ) { - // selected text within content hover widget - return true; - } - if ( - !sticky - && target.type === MouseTargetType.CONTENT_WIDGET - && target.detail === ContentHoverWidget.ID - && this._contentWidget?.isColorPickerVisible - ) { - // though the hover is not sticky, the color picker is sticky - return true; - } - if ( - sticky - && target.type === MouseTargetType.OVERLAY_WIDGET - && target.detail === MarginHoverWidget.ID - ) { - // mouse moved on top of overlay hover widget - return true; - } - return false; - } - - private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { - - this._mouseMoveEvent = mouseEvent; - if (this._contentWidget?.isFocused || this._contentWidget?.isResizing) { - return; - } - if (this._hoverState.mouseDown && this._hoverState.contentHoverFocused) { - return; - } - const sticky = this._hoverSettings.sticky; - if (sticky && this._contentWidget?.isVisibleFromKeyboard) { - // Sticky mode is on and the hover has been shown via keyboard - // so moving the mouse has no effect - return; - } - - const mouseIsOverWidget = this._isMouseOverWidget(mouseEvent); - // If the mouse is over the widget and the hiding timeout is defined, then cancel it - if (mouseIsOverWidget) { - this._reactToEditorMouseMoveRunner.cancel(); - return; - } - - // If the mouse is not over the widget, and if sticky is on, - // then give it a grace period before reacting to the mouse event - const hidingDelay = this._hoverSettings.hidingDelay; - if (this._contentWidget?.isVisible && sticky && hidingDelay > 0) { - if (!this._reactToEditorMouseMoveRunner.isScheduled()) { - this._reactToEditorMouseMoveRunner.schedule(hidingDelay); - } - return; - } - this._reactToEditorMouseMove(mouseEvent); - } - - private _reactToEditorMouseMove(mouseEvent: IEditorMouseEvent | undefined): void { - - if (!mouseEvent) { - return; - } - - const target = mouseEvent.target; - const mouseOnDecorator = target.element?.classList.contains('colorpicker-color-decoration'); - const decoratorActivatedOn = this._editor.getOption(EditorOption.colorDecoratorsActivatedOn); - - const enabled = this._hoverSettings.enabled; - const activatedByDecoratorClick = this._hoverState.activatedByDecoratorClick; - if ( - ( - mouseOnDecorator && ( - (decoratorActivatedOn === 'click' && !activatedByDecoratorClick) || - (decoratorActivatedOn === 'hover' && !enabled && !_sticky) || - (decoratorActivatedOn === 'clickAndHover' && !enabled && !activatedByDecoratorClick)) - ) || ( - !mouseOnDecorator && !enabled && !activatedByDecoratorClick - ) - ) { - this._hideWidgets(); - return; - } - - const contentWidget = this._getOrCreateContentWidget(); - - if (contentWidget.showsOrWillShow(mouseEvent)) { - this._glyphWidget?.hide(); - return; - } - - if (target.type === MouseTargetType.GUTTER_GLYPH_MARGIN && target.position && target.detail.glyphMarginLane) { - this._contentWidget?.hide(); - const glyphWidget = this._getOrCreateGlyphWidget(); - glyphWidget.startShowingAt(target.position.lineNumber, target.detail.glyphMarginLane); - return; - } - if (target.type === MouseTargetType.GUTTER_LINE_NUMBERS && target.position) { - this._contentWidget?.hide(); - const glyphWidget = this._getOrCreateGlyphWidget(); - glyphWidget.startShowingAt(target.position.lineNumber, 'lineNo'); - return; - } - if (_sticky) { - return; - } - this._hideWidgets(); - } - - private _onKeyDown(e: IKeyboardEvent): void { - if (!this._editor.hasModel()) { - return; - } - - const resolvedKeyboardEvent = this._keybindingService.softDispatch(e, this._editor.getDomNode()); - - // If the beginning of a multi-chord keybinding is pressed, - // or the command aims to focus the hover, - // set the variable to true, otherwise false - const mightTriggerFocus = ( - resolvedKeyboardEvent.kind === ResultKind.MoreChordsNeeded || - (resolvedKeyboardEvent.kind === ResultKind.KbFound - && resolvedKeyboardEvent.commandId === 'editor.action.showHover' - && this._contentWidget?.isVisible - ) - ); - - if ( - e.keyCode === KeyCode.Ctrl - || e.keyCode === KeyCode.Alt - || e.keyCode === KeyCode.Meta - || e.keyCode === KeyCode.Shift - || mightTriggerFocus - ) { - // Do not hide hover when a modifier key is pressed - return; - } - - this._hideWidgets(); - } - - private _hideWidgets(): void { - if (_sticky) { - return; - } - if ( - ( - this._hoverState.mouseDown - && this._hoverState.contentHoverFocused - && this._contentWidget?.isColorPickerVisible - ) - || InlineSuggestionHintsContentWidget.dropDownVisible - ) { - return; - } - this._hoverState.activatedByDecoratorClick = false; - this._hoverState.contentHoverFocused = false; - this._glyphWidget?.hide(); - this._contentWidget?.hide(); - } - - private _getOrCreateContentWidget(): ContentHoverController { - if (!this._contentWidget) { - this._contentWidget = this._instantiationService.createInstance(ContentHoverController, this._editor); - } - return this._contentWidget; - } - - private _getOrCreateGlyphWidget(): MarginHoverWidget { - if (!this._glyphWidget) { - this._glyphWidget = new MarginHoverWidget(this._editor, this._languageService, this._openerService); - } - return this._glyphWidget; - } - - public hideContentHover(): void { - this._hideWidgets(); - } - - public showContentHover( - range: Range, - mode: HoverStartMode, - source: HoverStartSource, - focus: boolean, - activatedByColorDecoratorClick: boolean = false - ): void { - this._hoverState.activatedByDecoratorClick = activatedByColorDecoratorClick; - this._getOrCreateContentWidget().startShowingAtRange(range, mode, source, focus); - } - - public focus(): void { - this._contentWidget?.focus(); - } - - public scrollUp(): void { - this._contentWidget?.scrollUp(); - } - - public scrollDown(): void { - this._contentWidget?.scrollDown(); - } - - public scrollLeft(): void { - this._contentWidget?.scrollLeft(); - } - - public scrollRight(): void { - this._contentWidget?.scrollRight(); - } - - public pageUp(): void { - this._contentWidget?.pageUp(); - } - - public pageDown(): void { - this._contentWidget?.pageDown(); - } - - public goToTop(): void { - this._contentWidget?.goToTop(); - } - - public goToBottom(): void { - this._contentWidget?.goToBottom(); - } - - public getWidgetContent(): string | undefined { - return this._contentWidget?.getWidgetContent(); - } - - public get isColorPickerVisible(): boolean | undefined { - return this._contentWidget?.isColorPickerVisible; - } - - public get isHoverVisible(): boolean | undefined { - return this._contentWidget?.isVisible; - } - - public override dispose(): void { - super.dispose(); - this._unhookListeners(); - this._listenersStore.dispose(); - this._glyphWidget?.dispose(); - this._contentWidget?.dispose(); - } -} - -enum HoverFocusBehavior { - NoAutoFocus = 'noAutoFocus', - FocusIfVisible = 'focusIfVisible', - AutoFocusImmediately = 'autoFocusImmediately' -} - -class ShowOrFocusHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.showHover', - label: nls.localize({ - key: 'showOrFocusHover', - comment: [ - 'Label for action that will trigger the showing/focusing of a hover in the editor.', - 'If the hover is not visible, it will show the hover.', - 'This allows for users to show the hover without using the mouse.' - ] - }, "Show or Focus Hover"), - metadata: { - description: `Show or Focus Hover`, - args: [{ - name: 'args', - schema: { - type: 'object', - properties: { - 'focus': { - description: 'Controls if and when the hover should take focus upon being triggered by this action.', - enum: [HoverFocusBehavior.NoAutoFocus, HoverFocusBehavior.FocusIfVisible, HoverFocusBehavior.AutoFocusImmediately], - enumDescriptions: [ - nls.localize('showOrFocusHover.focus.noAutoFocus', 'The hover will not automatically take focus.'), - nls.localize('showOrFocusHover.focus.focusIfVisible', 'The hover will take focus only if it is already visible.'), - nls.localize('showOrFocusHover.focus.autoFocusImmediately', 'The hover will automatically take focus when it appears.'), - ], - default: HoverFocusBehavior.FocusIfVisible, - } - }, - } - }] - }, - alias: 'Show or Focus Hover', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI), - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { - if (!editor.hasModel()) { - return; - } - - const controller = HoverController.get(editor); - if (!controller) { - return; - } - - const focusArgument = args?.focus; - let focusOption = HoverFocusBehavior.FocusIfVisible; - if (Object.values(HoverFocusBehavior).includes(focusArgument)) { - focusOption = focusArgument; - } else if (typeof focusArgument === 'boolean' && focusArgument) { - focusOption = HoverFocusBehavior.AutoFocusImmediately; - } - - const showContentHover = (focus: boolean) => { - const position = editor.getPosition(); - const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - controller.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Keyboard, focus); - }; - - const accessibilitySupportEnabled = editor.getOption(EditorOption.accessibilitySupport) === AccessibilitySupport.Enabled; - - if (controller.isHoverVisible) { - if (focusOption !== HoverFocusBehavior.NoAutoFocus) { - controller.focus(); - } else { - showContentHover(accessibilitySupportEnabled); - } - } else { - showContentHover(accessibilitySupportEnabled || focusOption === HoverFocusBehavior.AutoFocusImmediately); - } - } -} - -class ShowDefinitionPreviewHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.showDefinitionPreviewHover', - label: nls.localize({ - key: 'showDefinitionPreviewHover', - comment: [ - 'Label for action that will trigger the showing of definition preview hover in the editor.', - 'This allows for users to show the definition preview hover without using the mouse.' - ] - }, "Show Definition Preview Hover"), - alias: 'Show Definition Preview Hover', - precondition: undefined - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - const position = editor.getPosition(); - - if (!position) { - return; - } - - const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - const goto = GotoDefinitionAtPositionEditorContribution.get(editor); - if (!goto) { - return; - } - - const promise = goto.startFindDefinitionFromCursor(position); - promise.then(() => { - controller.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Keyboard, true); - }); - } -} - -class ScrollUpHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.scrollUpHover', - label: nls.localize({ - key: 'scrollUpHover', - comment: [ - 'Action that allows to scroll up in the hover widget with the up arrow when the hover widget is focused.' - ] - }, "Scroll Up Hover"), - alias: 'Scroll Up Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.UpArrow, - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.scrollUp(); - } -} - -class ScrollDownHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.scrollDownHover', - label: nls.localize({ - key: 'scrollDownHover', - comment: [ - 'Action that allows to scroll down in the hover widget with the up arrow when the hover widget is focused.' - ] - }, "Scroll Down Hover"), - alias: 'Scroll Down Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.DownArrow, - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.scrollDown(); - } -} - -class ScrollLeftHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.scrollLeftHover', - label: nls.localize({ - key: 'scrollLeftHover', - comment: [ - 'Action that allows to scroll left in the hover widget with the left arrow when the hover widget is focused.' - ] - }, "Scroll Left Hover"), - alias: 'Scroll Left Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.LeftArrow, - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.scrollLeft(); - } -} - -class ScrollRightHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.scrollRightHover', - label: nls.localize({ - key: 'scrollRightHover', - comment: [ - 'Action that allows to scroll right in the hover widget with the right arrow when the hover widget is focused.' - ] - }, "Scroll Right Hover"), - alias: 'Scroll Right Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.RightArrow, - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.scrollRight(); - } -} - -class PageUpHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.pageUpHover', - label: nls.localize({ - key: 'pageUpHover', - comment: [ - 'Action that allows to page up in the hover widget with the page up command when the hover widget is focused.' - ] - }, "Page Up Hover"), - alias: 'Page Up Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.PageUp, - secondary: [KeyMod.Alt | KeyCode.UpArrow], - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.pageUp(); - } -} - - -class PageDownHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.pageDownHover', - label: nls.localize({ - key: 'pageDownHover', - comment: [ - 'Action that allows to page down in the hover widget with the page down command when the hover widget is focused.' - ] - }, "Page Down Hover"), - alias: 'Page Down Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.PageDown, - secondary: [KeyMod.Alt | KeyCode.DownArrow], - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.pageDown(); - } -} - -class GoToTopHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.goToTopHover', - label: nls.localize({ - key: 'goToTopHover', - comment: [ - 'Action that allows to go to the top of the hover widget with the home command when the hover widget is focused.' - ] - }, "Go To Top Hover"), - alias: 'Go To Bottom Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.Home, - secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.goToTop(); - } -} - - -class GoToBottomHoverAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.goToBottomHover', - label: nls.localize({ - key: 'goToBottomHover', - comment: [ - 'Action that allows to go to the bottom in the hover widget with the end command when the hover widget is focused.' - ] - }, "Go To Bottom Hover"), - alias: 'Go To Bottom Hover', - precondition: EditorContextKeys.hoverFocused, - kbOpts: { - kbExpr: EditorContextKeys.hoverFocused, - primary: KeyCode.End, - secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const controller = HoverController.get(editor); - if (!controller) { - return; - } - controller.goToBottom(); - } -} - -registerEditorContribution(HoverController.ID, HoverController, EditorContributionInstantiation.BeforeFirstInteraction); -registerEditorAction(ShowOrFocusHoverAction); -registerEditorAction(ShowDefinitionPreviewHoverAction); -registerEditorAction(ScrollUpHoverAction); -registerEditorAction(ScrollDownHoverAction); -registerEditorAction(ScrollLeftHoverAction); -registerEditorAction(ScrollRightHoverAction); -registerEditorAction(PageUpHoverAction); -registerEditorAction(PageDownHoverAction); -registerEditorAction(GoToTopHoverAction); -registerEditorAction(GoToBottomHoverAction); -HoverParticipantRegistry.register(MarkdownHoverParticipant); -HoverParticipantRegistry.register(MarkerHoverParticipant); - -// theming -registerThemingParticipant((theme, collector) => { - const hoverBorder = theme.getColor(editorHoverBorder); - if (hoverBorder) { - collector.addRule(`.monaco-editor .monaco-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); - collector.addRule(`.monaco-editor .monaco-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); - collector.addRule(`.monaco-editor .monaco-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); - } -}); diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts new file mode 100644 index 00000000000..d17df615a84 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/nls'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { AccessibleViewType, AccessibleViewProviderId, AccessibleContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; +import { DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { Action, IAction } from 'vs/base/common/actions'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { labelForHoverVerbosityAction } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; + +namespace HoverAccessibilityHelpNLS { + export const increaseVerbosity = localize('increaseVerbosity', '- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command.', ``); + export const decreaseVerbosity = localize('decreaseVerbosity', '- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.', ``); +} + +export class HoverAccessibleView implements IAccessibleViewImplentation { + + public readonly type = AccessibleViewType.View; + public readonly priority = 95; + public readonly name = 'hover'; + public readonly when = EditorContextKeys.hoverFocused; + + getProvider(accessor: ServicesAccessor): AccessibleContentProvider | undefined { + const codeEditorService = accessor.get(ICodeEditorService); + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + throw new Error('No active or focused code editor'); + } + const hoverController = HoverController.get(codeEditor); + if (!hoverController) { + return; + } + const keybindingService = accessor.get(IKeybindingService); + return accessor.get(IInstantiationService).createInstance(HoverAccessibleViewProvider, keybindingService, codeEditor, hoverController); + } +} + +export class HoverAccessibilityHelp implements IAccessibleViewImplentation { + + public readonly priority = 100; + public readonly name = 'hover'; + public readonly type = AccessibleViewType.Help; + public readonly when = EditorContextKeys.hoverVisible; + + getProvider(accessor: ServicesAccessor): AccessibleContentProvider | undefined { + const codeEditorService = accessor.get(ICodeEditorService); + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + throw new Error('No active or focused code editor'); + } + const hoverController = HoverController.get(codeEditor); + if (!hoverController) { + return; + } + return accessor.get(IInstantiationService).createInstance(HoverAccessibilityHelpProvider, hoverController); + } +} + +abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider { + + abstract provideContent(): string; + abstract options: IAccessibleViewOptions; + + public readonly id = AccessibleViewProviderId.Hover; + public readonly verbositySettingKey = 'accessibility.verbosity.hover'; + + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + protected _focusedHoverPartIndex: number = -1; + + constructor(protected readonly _hoverController: HoverController) { + super(); + } + + public onOpen(): void { + if (!this._hoverController) { + return; + } + this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = true; + this._focusedHoverPartIndex = this._hoverController.focusedHoverPartIndex(); + this._register(this._hoverController.onHoverContentsChanged(() => { + this._onDidChangeContent.fire(); + })); + } + + public onClose(): void { + if (!this._hoverController) { + return; + } + if (this._focusedHoverPartIndex === -1) { + this._hoverController.focus(); + } else { + this._hoverController.focusHoverPartWithIndex(this._focusedHoverPartIndex); + } + this._focusedHoverPartIndex = -1; + this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = false; + } + + provideContentAtIndex(focusedHoverIndex: number, includeVerbosityActions: boolean): string { + if (focusedHoverIndex !== -1) { + const accessibleContent = this._hoverController.getAccessibleWidgetContentAtIndex(focusedHoverIndex); + if (accessibleContent === undefined) { + return ''; + } + const contents: string[] = []; + if (includeVerbosityActions) { + contents.push(...this._descriptionsOfVerbosityActionsForIndex(focusedHoverIndex)); + } + contents.push(accessibleContent); + return contents.join('\n'); + } else { + const accessibleContent = this._hoverController.getAccessibleWidgetContent(); + if (accessibleContent === undefined) { + return ''; + } + const contents: string[] = []; + contents.push(accessibleContent); + return contents.join('\n'); + } + } + + private _descriptionsOfVerbosityActionsForIndex(index: number): string[] { + const content: string[] = []; + const descriptionForIncreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Increase, index); + if (descriptionForIncreaseAction !== undefined) { + content.push(descriptionForIncreaseAction); + } + const descriptionForDecreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Decrease, index); + if (descriptionForDecreaseAction !== undefined) { + content.push(descriptionForDecreaseAction); + } + return content; + } + + private _descriptionOfVerbosityActionForIndex(action: HoverVerbosityAction, index: number): string | undefined { + const isActionSupported = this._hoverController.doesHoverAtIndexSupportVerbosityAction(index, action); + if (!isActionSupported) { + return; + } + switch (action) { + case HoverVerbosityAction.Increase: + return HoverAccessibilityHelpNLS.increaseVerbosity; + case HoverVerbosityAction.Decrease: + return HoverAccessibilityHelpNLS.decreaseVerbosity; + } + } +} + +export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider { + + public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; + + constructor(hoverController: HoverController) { + super(hoverController); + } + + provideContent(): string { + return this.provideContentAtIndex(this._focusedHoverPartIndex, true); + } +} + +export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider { + + public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.View }; + + constructor( + private readonly _keybindingService: IKeybindingService, + private readonly _editor: ICodeEditor, + hoverController: HoverController, + ) { + super(hoverController); + this._initializeOptions(this._editor, hoverController); + } + + public provideContent(): string { + return this.provideContentAtIndex(this._focusedHoverPartIndex, false); + } + + public get actions(): IAction[] { + const actions: IAction[] = []; + actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Increase)); + actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Decrease)); + return actions; + } + + private _getActionFor(editor: ICodeEditor, action: HoverVerbosityAction): IAction { + let actionId: string; + let accessibleActionId: string; + let actionCodicon: ThemeIcon; + switch (action) { + case HoverVerbosityAction.Increase: + actionId = INCREASE_HOVER_VERBOSITY_ACTION_ID; + accessibleActionId = INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID; + actionCodicon = Codicon.add; + break; + case HoverVerbosityAction.Decrease: + actionId = DECREASE_HOVER_VERBOSITY_ACTION_ID; + accessibleActionId = DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID; + actionCodicon = Codicon.remove; + break; + } + const actionLabel = labelForHoverVerbosityAction(this._keybindingService, action); + const actionEnabled = this._hoverController.doesHoverAtIndexSupportVerbosityAction(this._focusedHoverPartIndex, action); + return new Action(accessibleActionId, actionLabel, ThemeIcon.asClassName(actionCodicon), actionEnabled, () => { + editor.getAction(actionId)?.run({ index: this._focusedHoverPartIndex, focus: false }); + }); + } + + private _initializeOptions(editor: ICodeEditor, hoverController: HoverController): void { + const helpProvider = this._register(new HoverAccessibilityHelpProvider(hoverController)); + this.options.language = editor.getModel()?.getLanguageId(); + this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._focusedHoverPartIndex, true); }; + } +} + +export class ExtHoverAccessibleView implements IAccessibleViewImplentation { + public readonly type = AccessibleViewType.View; + public readonly priority = 90; + public readonly name = 'extension-hover'; + + getProvider(accessor: ServicesAccessor): AccessibleContentProvider | undefined { + const contextViewService = accessor.get(IContextViewService); + const contextViewElement = contextViewService.getContextViewElement(); + const extensionHoverContent = contextViewElement?.textContent ?? undefined; + const hoverService = accessor.get(IHoverService); + + if (contextViewElement.classList.contains('accessible-view-container') || !extensionHoverContent) { + // The accessible view, itself, uses the context view service to display the text. We don't want to read that. + return; + } + return new AccessibleContentProvider( + AccessibleViewProviderId.Hover, + { language: 'typescript', type: AccessibleViewType.View }, + () => { return extensionHoverContent; }, + () => { + hoverService.showAndFocusLastHover(); + }, + 'accessibility.verbosity.hover', + ); + } +} diff --git a/src/vs/editor/contrib/hover/browser/hoverActionIds.ts b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts new file mode 100644 index 00000000000..2ade6360ac1 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; + +export const SHOW_OR_FOCUS_HOVER_ACTION_ID = 'editor.action.showHover'; +export const SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID = 'editor.action.showDefinitionPreviewHover'; +export const SCROLL_UP_HOVER_ACTION_ID = 'editor.action.scrollUpHover'; +export const SCROLL_DOWN_HOVER_ACTION_ID = 'editor.action.scrollDownHover'; +export const SCROLL_LEFT_HOVER_ACTION_ID = 'editor.action.scrollLeftHover'; +export const SCROLL_RIGHT_HOVER_ACTION_ID = 'editor.action.scrollRightHover'; +export const PAGE_UP_HOVER_ACTION_ID = 'editor.action.pageUpHover'; +export const PAGE_DOWN_HOVER_ACTION_ID = 'editor.action.pageDownHover'; +export const GO_TO_TOP_HOVER_ACTION_ID = 'editor.action.goToTopHover'; +export const GO_TO_BOTTOM_HOVER_ACTION_ID = 'editor.action.goToBottomHover'; +export const INCREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.increaseHoverVerbosityLevel'; +export const INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.increaseHoverVerbosityLevelFromAccessibleView'; +export const INCREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'increaseHoverVerbosityLevel', comment: ['Label for action that will increase the hover verbosity level.'] }, "Increase Hover Verbosity Level"); +export const DECREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevel'; +export const DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevelFromAccessibleView'; +export const DECREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'decreaseHoverVerbosityLevel', comment: ['Label for action that will decrease the hover verbosity level.'] }, "Decrease Hover Verbosity Level"); diff --git a/src/vs/editor/contrib/hover/browser/hoverActions.ts b/src/vs/editor/contrib/hover/browser/hoverActions.ts new file mode 100644 index 00000000000..37eea40441a --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverActions.ts @@ -0,0 +1,463 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DECREASE_HOVER_VERBOSITY_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_LABEL, GO_TO_BOTTOM_HOVER_ACTION_ID, GO_TO_TOP_HOVER_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_LABEL, PAGE_DOWN_HOVER_ACTION_ID, PAGE_UP_HOVER_ACTION_ID, SCROLL_DOWN_HOVER_ACTION_ID, SCROLL_LEFT_HOVER_ACTION_ID, SCROLL_RIGHT_HOVER_ACTION_ID, SCROLL_UP_HOVER_ACTION_ID, SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; +import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; +import * as nls from 'vs/nls'; +import 'vs/css!./hover'; + +enum HoverFocusBehavior { + NoAutoFocus = 'noAutoFocus', + FocusIfVisible = 'focusIfVisible', + AutoFocusImmediately = 'autoFocusImmediately' +} + +export class ShowOrFocusHoverAction extends EditorAction { + + constructor() { + super({ + id: SHOW_OR_FOCUS_HOVER_ACTION_ID, + label: nls.localize({ + key: 'showOrFocusHover', + comment: [ + 'Label for action that will trigger the showing/focusing of a hover in the editor.', + 'If the hover is not visible, it will show the hover.', + 'This allows for users to show the hover without using the mouse.' + ] + }, "Show or Focus Hover"), + metadata: { + description: nls.localize2('showOrFocusHoverDescription', 'Show or focus the editor hover which shows documentation, references, and other content for a symbol at the current cursor position.'), + args: [{ + name: 'args', + schema: { + type: 'object', + properties: { + 'focus': { + description: 'Controls if and when the hover should take focus upon being triggered by this action.', + enum: [HoverFocusBehavior.NoAutoFocus, HoverFocusBehavior.FocusIfVisible, HoverFocusBehavior.AutoFocusImmediately], + enumDescriptions: [ + nls.localize('showOrFocusHover.focus.noAutoFocus', 'The hover will not automatically take focus.'), + nls.localize('showOrFocusHover.focus.focusIfVisible', 'The hover will take focus only if it is already visible.'), + nls.localize('showOrFocusHover.focus.autoFocusImmediately', 'The hover will automatically take focus when it appears.'), + ], + default: HoverFocusBehavior.FocusIfVisible, + } + }, + } + }] + }, + alias: 'Show or Focus Hover', + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI), + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + if (!editor.hasModel()) { + return; + } + + const controller = HoverController.get(editor); + if (!controller) { + return; + } + + const focusArgument = args?.focus; + let focusOption = HoverFocusBehavior.FocusIfVisible; + if (Object.values(HoverFocusBehavior).includes(focusArgument)) { + focusOption = focusArgument; + } else if (typeof focusArgument === 'boolean' && focusArgument) { + focusOption = HoverFocusBehavior.AutoFocusImmediately; + } + + const showContentHover = (focus: boolean) => { + const position = editor.getPosition(); + const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); + controller.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Keyboard, focus); + }; + + const accessibilitySupportEnabled = editor.getOption(EditorOption.accessibilitySupport) === AccessibilitySupport.Enabled; + + if (controller.isHoverVisible) { + if (focusOption !== HoverFocusBehavior.NoAutoFocus) { + controller.focus(); + } else { + showContentHover(accessibilitySupportEnabled); + } + } else { + showContentHover(accessibilitySupportEnabled || focusOption === HoverFocusBehavior.AutoFocusImmediately); + } + } +} + +export class ShowDefinitionPreviewHoverAction extends EditorAction { + + constructor() { + super({ + id: SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID, + label: nls.localize({ + key: 'showDefinitionPreviewHover', + comment: [ + 'Label for action that will trigger the showing of definition preview hover in the editor.', + 'This allows for users to show the definition preview hover without using the mouse.' + ] + }, "Show Definition Preview Hover"), + alias: 'Show Definition Preview Hover', + precondition: undefined, + metadata: { + description: nls.localize2('showDefinitionPreviewHoverDescription', 'Show the definition preview hover in the editor.'), + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + const position = editor.getPosition(); + + if (!position) { + return; + } + + const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); + const goto = GotoDefinitionAtPositionEditorContribution.get(editor); + if (!goto) { + return; + } + + const promise = goto.startFindDefinitionFromCursor(position); + promise.then(() => { + controller.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Keyboard, true); + }); + } +} + +export class ScrollUpHoverAction extends EditorAction { + + constructor() { + super({ + id: SCROLL_UP_HOVER_ACTION_ID, + label: nls.localize({ + key: 'scrollUpHover', + comment: [ + 'Action that allows to scroll up in the hover widget with the up arrow when the hover widget is focused.' + ] + }, "Scroll Up Hover"), + alias: 'Scroll Up Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.UpArrow, + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('scrollUpHoverDescription', 'Scroll up the editor hover.') + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.scrollUp(); + } +} + +export class ScrollDownHoverAction extends EditorAction { + + constructor() { + super({ + id: SCROLL_DOWN_HOVER_ACTION_ID, + label: nls.localize({ + key: 'scrollDownHover', + comment: [ + 'Action that allows to scroll down in the hover widget with the up arrow when the hover widget is focused.' + ] + }, "Scroll Down Hover"), + alias: 'Scroll Down Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.DownArrow, + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('scrollDownHoverDescription', 'Scroll down the editor hover.'), + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.scrollDown(); + } +} + +export class ScrollLeftHoverAction extends EditorAction { + + constructor() { + super({ + id: SCROLL_LEFT_HOVER_ACTION_ID, + label: nls.localize({ + key: 'scrollLeftHover', + comment: [ + 'Action that allows to scroll left in the hover widget with the left arrow when the hover widget is focused.' + ] + }, "Scroll Left Hover"), + alias: 'Scroll Left Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.LeftArrow, + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('scrollLeftHoverDescription', 'Scroll left the editor hover.'), + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.scrollLeft(); + } +} + +export class ScrollRightHoverAction extends EditorAction { + + constructor() { + super({ + id: SCROLL_RIGHT_HOVER_ACTION_ID, + label: nls.localize({ + key: 'scrollRightHover', + comment: [ + 'Action that allows to scroll right in the hover widget with the right arrow when the hover widget is focused.' + ] + }, "Scroll Right Hover"), + alias: 'Scroll Right Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.RightArrow, + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('scrollRightHoverDescription', 'Scroll right the editor hover.') + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.scrollRight(); + } +} + +export class PageUpHoverAction extends EditorAction { + + constructor() { + super({ + id: PAGE_UP_HOVER_ACTION_ID, + label: nls.localize({ + key: 'pageUpHover', + comment: [ + 'Action that allows to page up in the hover widget with the page up command when the hover widget is focused.' + ] + }, "Page Up Hover"), + alias: 'Page Up Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.PageUp, + secondary: [KeyMod.Alt | KeyCode.UpArrow], + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('pageUpHoverDescription', 'Page up the editor hover.'), + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.pageUp(); + } +} + +export class PageDownHoverAction extends EditorAction { + + constructor() { + super({ + id: PAGE_DOWN_HOVER_ACTION_ID, + label: nls.localize({ + key: 'pageDownHover', + comment: [ + 'Action that allows to page down in the hover widget with the page down command when the hover widget is focused.' + ] + }, "Page Down Hover"), + alias: 'Page Down Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.PageDown, + secondary: [KeyMod.Alt | KeyCode.DownArrow], + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('pageDownHoverDescription', 'Page down the editor hover.'), + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.pageDown(); + } +} + +export class GoToTopHoverAction extends EditorAction { + + constructor() { + super({ + id: GO_TO_TOP_HOVER_ACTION_ID, + label: nls.localize({ + key: 'goToTopHover', + comment: [ + 'Action that allows to go to the top of the hover widget with the home command when the hover widget is focused.' + ] + }, "Go To Top Hover"), + alias: 'Go To Bottom Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.Home, + secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('goToTopHoverDescription', 'Go to the top of the editor hover.'), + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.goToTop(); + } +} + + +export class GoToBottomHoverAction extends EditorAction { + + constructor() { + super({ + id: GO_TO_BOTTOM_HOVER_ACTION_ID, + label: nls.localize({ + key: 'goToBottomHover', + comment: [ + 'Action that allows to go to the bottom in the hover widget with the end command when the hover widget is focused.' + ] + }, "Go To Bottom Hover"), + alias: 'Go To Bottom Hover', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyCode.End, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], + weight: KeybindingWeight.EditorContrib + }, + metadata: { + description: nls.localize2('goToBottomHoverDescription', 'Go to the bottom of the editor hover.') + }, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const controller = HoverController.get(editor); + if (!controller) { + return; + } + controller.goToBottom(); + } +} + +export class IncreaseHoverVerbosityLevel extends EditorAction { + + constructor() { + super({ + id: INCREASE_HOVER_VERBOSITY_ACTION_ID, + label: INCREASE_HOVER_VERBOSITY_ACTION_LABEL, + alias: 'Increase Hover Verbosity Level', + precondition: EditorContextKeys.hoverVisible + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { + const hoverController = HoverController.get(editor); + if (!hoverController) { + return; + } + const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); + hoverController.updateHoverVerbosityLevel(HoverVerbosityAction.Increase, index, args?.focus); + } +} + +export class DecreaseHoverVerbosityLevel extends EditorAction { + + constructor() { + super({ + id: DECREASE_HOVER_VERBOSITY_ACTION_ID, + label: DECREASE_HOVER_VERBOSITY_ACTION_LABEL, + alias: 'Decrease Hover Verbosity Level', + precondition: EditorContextKeys.hoverVisible + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { + const hoverController = HoverController.get(editor); + if (!hoverController) { + return; + } + const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); + HoverController.get(editor)?.updateHoverVerbosityLevel(HoverVerbosityAction.Decrease, index, args?.focus); + } +} diff --git a/src/vs/editor/contrib/hover/browser/hoverContribution.ts b/src/vs/editor/contrib/hover/browser/hoverContribution.ts new file mode 100644 index 00000000000..bf24cdc1c69 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverContribution.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 { DecreaseHoverVerbosityLevel, GoToBottomHoverAction, GoToTopHoverAction, IncreaseHoverVerbosityLevel, PageDownHoverAction, PageUpHoverAction, ScrollDownHoverAction, ScrollLeftHoverAction, ScrollRightHoverAction, ScrollUpHoverAction, ShowDefinitionPreviewHoverAction, ShowOrFocusHoverAction } from 'vs/editor/contrib/hover/browser/hoverActions'; +import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; +import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHoverParticipant'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import 'vs/css!./hover'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ExtHoverAccessibleView, HoverAccessibilityHelp, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews'; + +registerEditorContribution(HoverController.ID, HoverController, EditorContributionInstantiation.BeforeFirstInteraction); +registerEditorAction(ShowOrFocusHoverAction); +registerEditorAction(ShowDefinitionPreviewHoverAction); +registerEditorAction(ScrollUpHoverAction); +registerEditorAction(ScrollDownHoverAction); +registerEditorAction(ScrollLeftHoverAction); +registerEditorAction(ScrollRightHoverAction); +registerEditorAction(PageUpHoverAction); +registerEditorAction(PageDownHoverAction); +registerEditorAction(GoToTopHoverAction); +registerEditorAction(GoToBottomHoverAction); +registerEditorAction(IncreaseHoverVerbosityLevel); +registerEditorAction(DecreaseHoverVerbosityLevel); +HoverParticipantRegistry.register(MarkdownHoverParticipant); +HoverParticipantRegistry.register(MarkerHoverParticipant); + +// theming +registerThemingParticipant((theme, collector) => { + const hoverBorder = theme.getColor(editorHoverBorder); + if (hoverBorder) { + collector.addRule(`.monaco-editor .monaco-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); + collector.addRule(`.monaco-editor .monaco-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); + collector.addRule(`.monaco-editor .monaco-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); + } +}); +AccessibleViewRegistry.register(new HoverAccessibleView()); +AccessibleViewRegistry.register(new HoverAccessibilityHelp()); +AccessibleViewRegistry.register(new ExtHoverAccessibleView()); diff --git a/src/vs/editor/contrib/hover/browser/hoverController.ts b/src/vs/editor/contrib/hover/browser/hoverController.ts new file mode 100644 index 00000000000..21b3281dd1d --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverController.ts @@ -0,0 +1,489 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorContribution, IScrollEvent } from 'vs/editor/common/editorCommon'; +import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { isMousePositionWithinElement } from 'vs/editor/contrib/hover/browser/hoverUtils'; +import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController'; +import 'vs/css!./hover'; +import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHoverWidget'; +import { Emitter } from 'vs/base/common/event'; + +// sticky hover widget which doesn't disappear on focus out and such +const _sticky = false + // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this + ; + +interface IHoverSettings { + readonly enabled: boolean; + readonly sticky: boolean; + readonly hidingDelay: number; +} + +interface IHoverState { + mouseDown: boolean; + activatedByDecoratorClick: boolean; +} + +const enum HoverWidgetType { + Content, + Glyph, +} + +export class HoverController extends Disposable implements IEditorContribution { + + private readonly _onHoverContentsChanged = this._register(new Emitter()); + public readonly onHoverContentsChanged = this._onHoverContentsChanged.event; + + public static readonly ID = 'editor.contrib.hover'; + + public shouldKeepOpenOnEditorMouseMoveOrLeave: boolean = false; + + private readonly _listenersStore = new DisposableStore(); + + private _glyphWidget: MarginHoverWidget | undefined; + private _contentWidget: ContentHoverController | undefined; + + private _mouseMoveEvent: IEditorMouseEvent | undefined; + private _reactToEditorMouseMoveRunner: RunOnceScheduler; + + private _hoverSettings!: IHoverSettings; + private _hoverState: IHoverState = { + mouseDown: false, + activatedByDecoratorClick: false + }; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService + ) { + super(); + this._reactToEditorMouseMoveRunner = this._register( + new RunOnceScheduler( + () => this._reactToEditorMouseMove(this._mouseMoveEvent), 0 + ) + ); + this._hookListeners(); + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (e.hasChanged(EditorOption.hover)) { + this._unhookListeners(); + this._hookListeners(); + } + })); + } + + static get(editor: ICodeEditor): HoverController | null { + return editor.getContribution(HoverController.ID); + } + + private _hookListeners(): void { + + const hoverOpts = this._editor.getOption(EditorOption.hover); + this._hoverSettings = { + enabled: hoverOpts.enabled, + sticky: hoverOpts.sticky, + hidingDelay: hoverOpts.delay + }; + + if (hoverOpts.enabled) { + this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); + this._listenersStore.add(this._editor.onMouseUp(() => this._onEditorMouseUp())); + this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); + this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); + } else { + this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); + this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); + } + + this._listenersStore.add(this._editor.onMouseLeave((e) => this._onEditorMouseLeave(e))); + this._listenersStore.add(this._editor.onDidChangeModel(() => { + this._cancelScheduler(); + this._hideWidgets(); + })); + this._listenersStore.add(this._editor.onDidChangeModelContent(() => this._cancelScheduler())); + this._listenersStore.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e))); + } + + private _unhookListeners(): void { + this._listenersStore.clear(); + } + + private _cancelScheduler() { + this._mouseMoveEvent = undefined; + this._reactToEditorMouseMoveRunner.cancel(); + } + + private _onEditorScrollChanged(e: IScrollEvent): void { + if (e.scrollTopChanged || e.scrollLeftChanged) { + this._hideWidgets(); + } + } + + private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { + + this._hoverState.mouseDown = true; + + const shouldNotHideCurrentHoverWidget = this._shouldNotHideCurrentHoverWidget(mouseEvent); + if (shouldNotHideCurrentHoverWidget) { + return; + } + + this._hideWidgets(); + } + + private _shouldNotHideCurrentHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { + return this._isMouseOnContentHoverWidget(mouseEvent) + || this._isMouseOnMarginHoverWidget(mouseEvent) + || this._isContentWidgetResizing(); + } + + private _isMouseOnMarginHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { + const marginHoverWidgetNode = this._glyphWidget?.getDomNode(); + if (marginHoverWidgetNode) { + return isMousePositionWithinElement(marginHoverWidgetNode, mouseEvent.event.posx, mouseEvent.event.posy); + } + return false; + } + + private _isMouseOnContentHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { + const contentWidgetNode = this._contentWidget?.getDomNode(); + if (contentWidgetNode) { + return isMousePositionWithinElement(contentWidgetNode, mouseEvent.event.posx, mouseEvent.event.posy); + } + return false; + } + + private _onEditorMouseUp(): void { + this._hoverState.mouseDown = false; + } + + private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void { + if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) { + return; + } + + this._cancelScheduler(); + + const shouldNotHideCurrentHoverWidget = this._shouldNotHideCurrentHoverWidget(mouseEvent); + if (shouldNotHideCurrentHoverWidget) { + return; + } + if (_sticky) { + return; + } + this._hideWidgets(); + } + + private _shouldNotRecomputeCurrentHoverWidget(mouseEvent: IEditorMouseEvent): boolean { + + const isHoverSticky = this._hoverSettings.sticky; + + const isMouseOnStickyMarginHoverWidget = (mouseEvent: IEditorMouseEvent, isHoverSticky: boolean): boolean => { + const isMouseOnMarginHoverWidget = this._isMouseOnMarginHoverWidget(mouseEvent); + return isHoverSticky && isMouseOnMarginHoverWidget; + }; + const isMouseOnStickyContentHoverWidget = (mouseEvent: IEditorMouseEvent, isHoverSticky: boolean): boolean => { + const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent); + return isHoverSticky && isMouseOnContentHoverWidget; + }; + const isMouseOnColorPicker = (mouseEvent: IEditorMouseEvent): boolean => { + const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent); + const isColorPickerVisible = this._contentWidget?.isColorPickerVisible ?? false; + return isMouseOnContentHoverWidget && isColorPickerVisible; + }; + // TODO@aiday-mar verify if the following is necessary code + const isTextSelectedWithinContentHoverWidget = (mouseEvent: IEditorMouseEvent, sticky: boolean): boolean => { + return (sticky + && this._contentWidget?.containsNode(mouseEvent.event.browserEvent.view?.document.activeElement) + && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed) ?? false; + }; + + return isMouseOnStickyMarginHoverWidget(mouseEvent, isHoverSticky) + || isMouseOnStickyContentHoverWidget(mouseEvent, isHoverSticky) + || isMouseOnColorPicker(mouseEvent) + || isTextSelectedWithinContentHoverWidget(mouseEvent, isHoverSticky); + } + + private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { + if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) { + return; + } + + this._mouseMoveEvent = mouseEvent; + if (this._contentWidget?.isFocused || this._contentWidget?.isResizing) { + return; + } + const sticky = this._hoverSettings.sticky; + if (sticky && this._contentWidget?.isVisibleFromKeyboard) { + // Sticky mode is on and the hover has been shown via keyboard + // so moving the mouse has no effect + return; + } + + const shouldNotRecomputeCurrentHoverWidget = this._shouldNotRecomputeCurrentHoverWidget(mouseEvent); + if (shouldNotRecomputeCurrentHoverWidget) { + this._reactToEditorMouseMoveRunner.cancel(); + return; + } + + const hidingDelay = this._hoverSettings.hidingDelay; + const isContentHoverWidgetVisible = this._contentWidget?.isVisible; + // If the mouse is not over the widget, and if sticky is on, + // then give it a grace period before reacting to the mouse event + const shouldRescheduleHoverComputation = isContentHoverWidgetVisible && sticky && hidingDelay > 0; + + if (shouldRescheduleHoverComputation) { + if (!this._reactToEditorMouseMoveRunner.isScheduled()) { + this._reactToEditorMouseMoveRunner.schedule(hidingDelay); + } + return; + } + this._reactToEditorMouseMove(mouseEvent); + } + + private _reactToEditorMouseMove(mouseEvent: IEditorMouseEvent | undefined): void { + + if (!mouseEvent) { + return; + } + + const target = mouseEvent.target; + const mouseOnDecorator = target.element?.classList.contains('colorpicker-color-decoration'); + const decoratorActivatedOn = this._editor.getOption(EditorOption.colorDecoratorsActivatedOn); + + const enabled = this._hoverSettings.enabled; + const activatedByDecoratorClick = this._hoverState.activatedByDecoratorClick; + if ( + ( + mouseOnDecorator && ( + (decoratorActivatedOn === 'click' && !activatedByDecoratorClick) || + (decoratorActivatedOn === 'hover' && !enabled && !_sticky) || + (decoratorActivatedOn === 'clickAndHover' && !enabled && !activatedByDecoratorClick)) + ) || ( + !mouseOnDecorator && !enabled && !activatedByDecoratorClick + ) + ) { + this._hideWidgets(); + return; + } + + const contentHoverShowsOrWillShow = this._tryShowHoverWidget(mouseEvent, HoverWidgetType.Content); + if (contentHoverShowsOrWillShow) { + return; + } + + const glyphWidgetShowsOrWillShow = this._tryShowHoverWidget(mouseEvent, HoverWidgetType.Glyph); + if (glyphWidgetShowsOrWillShow) { + return; + } + if (_sticky) { + return; + } + this._hideWidgets(); + } + + private _tryShowHoverWidget(mouseEvent: IEditorMouseEvent, hoverWidgetType: HoverWidgetType): boolean { + const contentWidget: IHoverWidget = this._getOrCreateContentWidget(); + const glyphWidget: IHoverWidget = this._getOrCreateGlyphWidget(); + let currentWidget: IHoverWidget; + let otherWidget: IHoverWidget; + switch (hoverWidgetType) { + case HoverWidgetType.Content: + currentWidget = contentWidget; + otherWidget = glyphWidget; + break; + case HoverWidgetType.Glyph: + currentWidget = glyphWidget; + otherWidget = contentWidget; + break; + default: + throw new Error(`HoverWidgetType ${hoverWidgetType} is unrecognized`); + } + + const showsOrWillShow = currentWidget.showsOrWillShow(mouseEvent); + if (showsOrWillShow) { + otherWidget.hide(); + } + return showsOrWillShow; + } + + private _onKeyDown(e: IKeyboardEvent): void { + if (!this._editor.hasModel()) { + return; + } + + const resolvedKeyboardEvent = this._keybindingService.softDispatch(e, this._editor.getDomNode()); + + // If the beginning of a multi-chord keybinding is pressed, + // or the command aims to focus the hover, + // set the variable to true, otherwise false + const shouldKeepHoverVisible = ( + resolvedKeyboardEvent.kind === ResultKind.MoreChordsNeeded || + (resolvedKeyboardEvent.kind === ResultKind.KbFound + && (resolvedKeyboardEvent.commandId === SHOW_OR_FOCUS_HOVER_ACTION_ID + || resolvedKeyboardEvent.commandId === INCREASE_HOVER_VERBOSITY_ACTION_ID + || resolvedKeyboardEvent.commandId === DECREASE_HOVER_VERBOSITY_ACTION_ID) + && this._contentWidget?.isVisible + ) + ); + + if ( + e.keyCode === KeyCode.Ctrl + || e.keyCode === KeyCode.Alt + || e.keyCode === KeyCode.Meta + || e.keyCode === KeyCode.Shift + || shouldKeepHoverVisible + ) { + // Do not hide hover when a modifier key is pressed + return; + } + + this._hideWidgets(); + } + + private _hideWidgets(): void { + if (_sticky) { + return; + } + if (( + this._hoverState.mouseDown + && this._contentWidget?.isColorPickerVisible + ) || InlineSuggestionHintsContentWidget.dropDownVisible) { + return; + } + this._hoverState.activatedByDecoratorClick = false; + this._glyphWidget?.hide(); + this._contentWidget?.hide(); + } + + private _getOrCreateContentWidget(): ContentHoverController { + if (!this._contentWidget) { + this._contentWidget = this._instantiationService.createInstance(ContentHoverController, this._editor); + this._listenersStore.add(this._contentWidget.onContentsChanged(() => this._onHoverContentsChanged.fire())); + } + return this._contentWidget; + } + + private _getOrCreateGlyphWidget(): MarginHoverWidget { + if (!this._glyphWidget) { + this._glyphWidget = this._instantiationService.createInstance(MarginHoverWidget, this._editor); + } + return this._glyphWidget; + } + + public hideContentHover(): void { + this._hideWidgets(); + } + + public showContentHover( + range: Range, + mode: HoverStartMode, + source: HoverStartSource, + focus: boolean, + activatedByColorDecoratorClick: boolean = false + ): void { + this._hoverState.activatedByDecoratorClick = activatedByColorDecoratorClick; + this._getOrCreateContentWidget().startShowingAtRange(range, mode, source, focus); + } + + private _isContentWidgetResizing(): boolean { + return this._contentWidget?.widget.isResizing || false; + } + + public focusedHoverPartIndex(): number { + return this._getOrCreateContentWidget().focusedHoverPartIndex(); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._getOrCreateContentWidget().doesHoverAtIndexSupportVerbosityAction(index, action); + } + + public updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): void { + this._getOrCreateContentWidget().updateHoverVerbosityLevel(action, index, focus); + } + + public focus(): void { + this._contentWidget?.focus(); + } + + public focusHoverPartWithIndex(index: number): void { + this._contentWidget?.focusHoverPartWithIndex(index); + } + + public scrollUp(): void { + this._contentWidget?.scrollUp(); + } + + public scrollDown(): void { + this._contentWidget?.scrollDown(); + } + + public scrollLeft(): void { + this._contentWidget?.scrollLeft(); + } + + public scrollRight(): void { + this._contentWidget?.scrollRight(); + } + + public pageUp(): void { + this._contentWidget?.pageUp(); + } + + public pageDown(): void { + this._contentWidget?.pageDown(); + } + + public goToTop(): void { + this._contentWidget?.goToTop(); + } + + public goToBottom(): void { + this._contentWidget?.goToBottom(); + } + + public getWidgetContent(): string | undefined { + return this._contentWidget?.getWidgetContent(); + } + + public getAccessibleWidgetContent(): string | undefined { + return this._contentWidget?.getAccessibleWidgetContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string | undefined { + return this._contentWidget?.getAccessibleWidgetContentAtIndex(index); + } + + public get isColorPickerVisible(): boolean | undefined { + return this._contentWidget?.isColorPickerVisible; + } + + public get isHoverVisible(): boolean | undefined { + return this._contentWidget?.isVisible; + } + + public override dispose(): void { + super.dispose(); + this._unhookListeners(); + this._listenersStore.dispose(); + this._glyphWidget?.dispose(); + this._contentWidget?.dispose(); + } +} diff --git a/src/vs/editor/contrib/hover/browser/hoverTypes.ts b/src/vs/editor/contrib/hover/browser/hoverTypes.ts index aadc1851a01..4b23a838478 100644 --- a/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -94,19 +94,7 @@ export interface IEditorHoverColorPickerWidget { layout(): void; } -export interface IEditorHoverRenderContext { - /** - * The fragment where dom elements should be attached. - */ - readonly fragment: DocumentFragment; - /** - * The status bar for actions for this hover. - */ - readonly statusBar: IEditorHoverStatusBar; - /** - * Set if the hover will render a color picker widget. - */ - setColorPicker(widget: IEditorHoverColorPickerWidget): void; +export interface IEditorHoverContext { /** * The contents rendered inside the fragment have been changed, which means that the hover should relayout. */ @@ -121,13 +109,58 @@ export interface IEditorHoverRenderContext { hide(): void; } +export interface IEditorHoverRenderContext extends IEditorHoverContext { + /** + * The fragment where dom elements should be attached. + */ + readonly fragment: DocumentFragment; + /** + * The status bar for actions for this hover. + */ + readonly statusBar: IEditorHoverStatusBar; +} + +export interface IRenderedHoverPart extends IDisposable { + /** + * The rendered hover part. + */ + hoverPart: T; + /** + * The HTML element containing the hover part. + */ + hoverElement: HTMLElement; +} + +export interface IRenderedHoverParts extends IDisposable { + /** + * Array of rendered hover parts. + */ + renderedHoverParts: IRenderedHoverPart[]; +} + +/** + * Default implementation of IRenderedHoverParts. + */ +export class RenderedHoverParts implements IRenderedHoverParts { + + constructor(public readonly renderedHoverParts: IRenderedHoverPart[]) { } + + dispose() { + for (const part of this.renderedHoverParts) { + part.dispose(); + } + } +} + export interface IEditorHoverParticipant { readonly hoverOrdinal: number; suggestHoverAnchor?(mouseEvent: IEditorMouseEvent): HoverAnchor | null; computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): T[]; computeAsync?(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): AsyncIterableObject; createLoadingMessage?(anchor: HoverAnchor): T | null; - renderHoverParts(context: IEditorHoverRenderContext, hoverParts: T[]): IDisposable; + renderHoverParts(context: IEditorHoverRenderContext, hoverParts: T[]): IRenderedHoverParts; + getAccessibleContent(hoverPart: T): string; + handleResize?(): void; } export type IEditorHoverParticipantCtor = IConstructorSignature; @@ -145,3 +178,17 @@ export const HoverParticipantRegistry = (new class HoverParticipantRegistry { } }()); + +export interface IHoverWidget { + /** + * Returns whether the hover widget is shown or should show in the future. + * If the widget should show, this triggers the display. + * @param mouseEvent editor mouse event + */ + showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean; + + /** + * Hides the hover. + */ + hide(): void; +} diff --git a/src/vs/editor/contrib/hover/browser/hoverUtils.ts b/src/vs/editor/contrib/hover/browser/hoverUtils.ts new file mode 100644 index 00000000000..3f9ab067c1d --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverUtils.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; + +export function isMousePositionWithinElement(element: HTMLElement, posx: number, posy: number): boolean { + const elementRect = dom.getDomNodePagePosition(element); + if (posx < elementRect.left + || posx > elementRect.left + elementRect.width + || posy < elementRect.top + || posy > elementRect.top + elementRect.height) { + return false; + } + return true; +} diff --git a/src/vs/editor/contrib/hover/browser/marginHoverComputer.ts b/src/vs/editor/contrib/hover/browser/marginHoverComputer.ts new file mode 100644 index 00000000000..0bc61948152 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/marginHoverComputer.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { asArray } from 'vs/base/common/arrays'; +import { IMarkdownString, isEmptyMarkdownString } from 'vs/base/common/htmlContent'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { GlyphMarginLane } from 'vs/editor/common/model'; + +export type LaneOrLineNumber = GlyphMarginLane | 'lineNo'; + +export interface IHoverMessage { + value: IMarkdownString; +} + +export class MarginHoverComputer implements IHoverComputer { + + private _lineNumber: number = -1; + private _laneOrLine: LaneOrLineNumber = GlyphMarginLane.Center; + + public get lineNumber(): number { + return this._lineNumber; + } + + public set lineNumber(value: number) { + this._lineNumber = value; + } + + public get lane(): LaneOrLineNumber { + return this._laneOrLine; + } + + public set lane(value: LaneOrLineNumber) { + this._laneOrLine = value; + } + + constructor( + private readonly _editor: ICodeEditor + ) { + } + + public computeSync(): IHoverMessage[] { + + const toHoverMessage = (contents: IMarkdownString): IHoverMessage => { + return { + value: contents + }; + }; + + const lineDecorations = this._editor.getLineDecorations(this._lineNumber); + + const result: IHoverMessage[] = []; + const isLineHover = this._laneOrLine === 'lineNo'; + if (!lineDecorations) { + return result; + } + + for (const d of lineDecorations) { + const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center; + if (!isLineHover && lane !== this._laneOrLine) { + continue; + } + + const hoverMessage = isLineHover ? d.options.lineNumberHoverMessage : d.options.glyphMarginHoverMessage; + if (!hoverMessage || isEmptyMarkdownString(hoverMessage)) { + continue; + } + + result.push(...asArray(hoverMessage).map(toHoverMessage)); + } + + return result; + } +} diff --git a/src/vs/editor/contrib/hover/browser/marginHover.ts b/src/vs/editor/contrib/hover/browser/marginHoverWidget.ts similarity index 73% rename from src/vs/editor/contrib/hover/browser/marginHover.ts rename to src/vs/editor/contrib/hover/browser/marginHoverWidget.ts index e9eb33604c1..e6fb9f4ce60 100644 --- a/src/vs/editor/contrib/hover/browser/marginHover.ts +++ b/src/vs/editor/contrib/hover/browser/marginHoverWidget.ts @@ -4,27 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { asArray } from 'vs/base/common/arrays'; -import { IMarkdownString, isEmptyMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IEditorMouseEvent, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { HoverOperation, HoverStartMode } from 'vs/editor/contrib/hover/browser/hoverOperation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; -import { GlyphMarginLane } from 'vs/editor/common/model'; +import { IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { IHoverMessage, LaneOrLineNumber, MarginHoverComputer } from 'vs/editor/contrib/hover/browser/marginHoverComputer'; +import { isMousePositionWithinElement } from 'vs/editor/contrib/hover/browser/hoverUtils'; const $ = dom.$; -export interface IHoverMessage { - value: IMarkdownString; -} - -type LaneOrLineNumber = GlyphMarginLane | 'lineNo'; - -export class MarginHoverWidget extends Disposable implements IOverlayWidget { +export class MarginHoverWidget extends Disposable implements IOverlayWidget, IHoverWidget { public static readonly ID = 'editor.contrib.modesGlyphHoverWidget'; @@ -41,8 +35,8 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget { constructor( editor: ICodeEditor, - languageService: ILanguageService, - openerService: IOpenerService, + @ILanguageService languageService: ILanguageService, + @IOpenerService openerService: IOpenerService, ) { super(); this._editor = editor; @@ -66,7 +60,9 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget { this._updateFont(); } })); - + this._register(dom.addStandardDisposableListener(this._hover.containerDomNode, 'mouseleave', (e) => { + this._onMouseLeave(e); + })); this._editor.addOverlayWidget(this); } @@ -101,7 +97,20 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget { } } - public startShowingAt(lineNumber: number, laneOrLine: LaneOrLineNumber): void { + public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean { + const target = mouseEvent.target; + if (target.type === MouseTargetType.GUTTER_GLYPH_MARGIN && target.detail.glyphMarginLane) { + this._startShowingAt(target.position.lineNumber, target.detail.glyphMarginLane); + return true; + } + if (target.type === MouseTargetType.GUTTER_LINE_NUMBERS) { + this._startShowingAt(target.position.lineNumber, 'lineNo'); + return true; + } + return false; + } + + private _startShowingAt(lineNumber: number, laneOrLine: LaneOrLineNumber): void { if (this._computer.lineNumber === lineNumber && this._computer.lane === laneOrLine) { // We have to show the widget at the exact same line number as before, so no work is needed return; @@ -175,64 +184,12 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget { this._hover.containerDomNode.style.left = `${left}px`; this._hover.containerDomNode.style.top = `${Math.max(Math.round(top), 0)}px`; } -} -class MarginHoverComputer implements IHoverComputer { - - private _lineNumber: number = -1; - private _laneOrLine: LaneOrLineNumber = GlyphMarginLane.Center; - - public get lineNumber(): number { - return this._lineNumber; - } - - public set lineNumber(value: number) { - this._lineNumber = value; - } - - public get lane(): LaneOrLineNumber { - return this._laneOrLine; - } - - public set lane(value: LaneOrLineNumber) { - this._laneOrLine = value; - } - - constructor( - private readonly _editor: ICodeEditor - ) { - } - - public computeSync(): IHoverMessage[] { - - const toHoverMessage = (contents: IMarkdownString): IHoverMessage => { - return { - value: contents - }; - }; - - const lineDecorations = this._editor.getLineDecorations(this._lineNumber); - - const result: IHoverMessage[] = []; - const isLineHover = this._laneOrLine === 'lineNo'; - if (!lineDecorations) { - return result; + private _onMouseLeave(e: MouseEvent): void { + const editorDomNode = this._editor.getDomNode(); + const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y); + if (isMousePositionOutsideOfEditor) { + this.hide(); } - - for (const d of lineDecorations) { - const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center; - if (!isLineHover && lane !== this._laneOrLine) { - continue; - } - - const hoverMessage = isLineHover ? d.options.lineNumberHoverMessage : d.options.glyphMarginHoverMessage; - if (!hoverMessage || isEmptyMarkdownString(hoverMessage)) { - continue; - } - - result.push(...asArray(hoverMessage).map(toHoverMessage)); - } - - return result; } } diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index 2227602d0d9..aaeb5796aa6 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -4,26 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { asArray } from 'vs/base/common/arrays'; -import { AsyncIterableObject } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { asArray, compareBy, numberComparator } from 'vs/base/common/arrays'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, isEmptyMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IModelDecoration } from 'vs/editor/common/model'; +import { IModelDecoration, ITextModel } from 'vs/editor/common/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { getHover } from 'vs/editor/contrib/hover/browser/getHover'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverRangeAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Hover, HoverContext, HoverProvider, HoverVerbosityAction } from 'vs/editor/common/languages'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ClickAction, HoverPosition, KeyDownAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { AsyncIterableObject } from 'vs/base/common/async'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { getHoverProviderResultsAsAsyncIterable } from 'vs/editor/contrib/hover/browser/getHover'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const $ = dom.$; +const increaseHoverVerbosityIcon = registerIcon('hover-increase-verbosity', Codicon.add, nls.localize('increaseHoverVerbosity', 'Icon for increaseing hover verbosity.')); +const decreaseHoverVerbosityIcon = registerIcon('hover-decrease-verbosity', Codicon.remove, nls.localize('decreaseHoverVerbosity', 'Icon for decreasing hover verbosity.')); export class MarkdownHover implements IHoverPart { @@ -32,7 +46,8 @@ export class MarkdownHover implements IHoverPart { public readonly range: Range, public readonly contents: IMarkdownString[], public readonly isBeforeContent: boolean, - public readonly ordinal: number + public readonly ordinal: number, + public readonly source: HoverSource | undefined = undefined, ) { } public isValidForHoverAnchor(anchor: HoverAnchor): boolean { @@ -44,16 +59,39 @@ export class MarkdownHover implements IHoverPart { } } +class HoverSource { + + constructor( + readonly hover: Hover, + readonly hoverProvider: HoverProvider, + readonly hoverPosition: Position, + ) { } + + public supportsVerbosityAction(hoverVerbosityAction: HoverVerbosityAction): boolean { + switch (hoverVerbosityAction) { + case HoverVerbosityAction.Increase: + return this.hover.canIncreaseVerbosity ?? false; + case HoverVerbosityAction.Decrease: + return this.hover.canDecreaseVerbosity ?? false; + } + } +} + export class MarkdownHoverParticipant implements IEditorHoverParticipant { public readonly hoverOrdinal: number = 3; + private _renderedHoverParts: MarkdownRenderedHoverParts | undefined; + constructor( protected readonly _editor: ICodeEditor, @ILanguageService private readonly _languageService: ILanguageService, @IOpenerService private readonly _openerService: IOpenerService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ILanguageFeaturesService protected readonly _languageFeaturesService: ILanguageFeaturesService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IHoverService private readonly _hoverService: IHoverService, + @ICommandService private readonly _commandService: ICommandService, ) { } public createLoadingMessage(anchor: HoverAnchor): MarkdownHover | null { @@ -120,52 +158,366 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant !isEmptyMarkdownString(item.hover.contents)) - .map(item => { - const rng = item.hover.range ? Range.lift(item.hover.range) : anchor.range; - return new MarkdownHover(this, rng, item.hover.contents, false, item.ordinal); - }); + const markdownHovers = this._getMarkdownHovers(hoverProviderRegistry, model, anchor, token); + return markdownHovers; } - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IDisposable { - return renderMarkdownHovers(context, hoverParts, this._editor, this._languageService, this._openerService); + private _getMarkdownHovers(hoverProviderRegistry: LanguageFeatureRegistry, model: ITextModel, anchor: HoverRangeAnchor, token: CancellationToken): AsyncIterableObject { + const position = anchor.range.getStartPosition(); + const hoverProviderResults = getHoverProviderResultsAsAsyncIterable(hoverProviderRegistry, model, position, token); + const markdownHovers = hoverProviderResults.filter(item => !isEmptyMarkdownString(item.hover.contents)) + .map(item => { + const range = item.hover.range ? Range.lift(item.hover.range) : anchor.range; + const hoverSource = new HoverSource(item.hover, item.provider, position); + return new MarkdownHover(this, range, item.hover.contents, false, item.ordinal, hoverSource); + }); + return markdownHovers; + } + + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IRenderedHoverParts { + this._renderedHoverParts = new MarkdownRenderedHoverParts( + hoverParts, + context.fragment, + this, + this._editor, + this._languageService, + this._openerService, + this._commandService, + this._keybindingService, + this._hoverService, + this._configurationService, + context.onContentsChanged + ); + return this._renderedHoverParts; + } + + public getAccessibleContent(hoverPart: MarkdownHover): string { + return this._renderedHoverParts?.getAccessibleContent(hoverPart) ?? ''; + } + + public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedHoverParts?.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action) ?? false; + } + + public updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise<{ hoverPart: MarkdownHover; hoverElement: HTMLElement } | undefined> { + return Promise.resolve(this._renderedHoverParts?.updateMarkdownHoverPartVerbosityLevel(action, index, focus)); + } +} + +class RenderedMarkdownHoverPart implements IRenderedHoverPart { + + constructor( + public readonly hoverPart: MarkdownHover, + public readonly hoverElement: HTMLElement, + public readonly disposables: DisposableStore, + ) { } + + get hoverAccessibleContent(): string { + return this.hoverElement.innerText.trim(); + } + + dispose(): void { + this.disposables.dispose(); + } +} + +class MarkdownRenderedHoverParts implements IRenderedHoverParts { + + public renderedHoverParts: RenderedMarkdownHoverPart[]; + + private _ongoingHoverOperations: Map = new Map(); + + private readonly _disposables = new DisposableStore(); + + constructor( + hoverParts: MarkdownHover[], + hoverPartsContainer: DocumentFragment, + private readonly _hoverParticipant: MarkdownHoverParticipant, + private readonly _editor: ICodeEditor, + private readonly _languageService: ILanguageService, + private readonly _openerService: IOpenerService, + private readonly _commandService: ICommandService, + private readonly _keybindingService: IKeybindingService, + private readonly _hoverService: IHoverService, + private readonly _configurationService: IConfigurationService, + private readonly _onFinishedRendering: () => void, + ) { + this.renderedHoverParts = this._renderHoverParts(hoverParts, hoverPartsContainer, this._onFinishedRendering); + this._disposables.add(toDisposable(() => { + this.renderedHoverParts.forEach(renderedHoverPart => { + renderedHoverPart.dispose(); + }); + this._ongoingHoverOperations.forEach(operation => { + operation.tokenSource.dispose(true); + }); + })); + } + + private _renderHoverParts( + hoverParts: MarkdownHover[], + hoverPartsContainer: DocumentFragment, + onFinishedRendering: () => void, + ): RenderedMarkdownHoverPart[] { + hoverParts.sort(compareBy(hover => hover.ordinal, numberComparator)); + return hoverParts.map(hoverPart => { + const renderedHoverPart = this._renderHoverPart(hoverPart, onFinishedRendering); + hoverPartsContainer.appendChild(renderedHoverPart.hoverElement); + return renderedHoverPart; + }); + } + + private _renderHoverPart( + hoverPart: MarkdownHover, + onFinishedRendering: () => void + ): RenderedMarkdownHoverPart { + + const renderedMarkdownPart = this._renderMarkdownHover(hoverPart, onFinishedRendering); + const renderedMarkdownElement = renderedMarkdownPart.hoverElement; + const hoverSource = hoverPart.source; + const disposables = new DisposableStore(); + disposables.add(renderedMarkdownPart); + + if (!hoverSource) { + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); + } + + const canIncreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Increase); + const canDecreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Decrease); + + if (!canIncreaseVerbosity && !canDecreaseVerbosity) { + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); + } + + const actionsContainer = $('div.verbosity-actions'); + renderedMarkdownElement.prepend(actionsContainer); + + disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Increase, canIncreaseVerbosity)); + disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Decrease, canDecreaseVerbosity)); + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); + } + + private _renderMarkdownHover( + markdownHover: MarkdownHover, + onFinishedRendering: () => void + ): IRenderedHoverPart { + const renderedMarkdownHover = renderMarkdownInContainer( + this._editor, + markdownHover, + this._languageService, + this._openerService, + onFinishedRendering, + ); + return renderedMarkdownHover; + } + + private _renderHoverExpansionAction(container: HTMLElement, action: HoverVerbosityAction, actionEnabled: boolean): DisposableStore { + const store = new DisposableStore(); + const isActionIncrease = action === HoverVerbosityAction.Increase; + const actionElement = dom.append(container, $(ThemeIcon.asCSSSelector(isActionIncrease ? increaseHoverVerbosityIcon : decreaseHoverVerbosityIcon))); + actionElement.tabIndex = 0; + const hoverDelegate = new WorkbenchHoverDelegate('mouse', false, { target: container, position: { hoverPosition: HoverPosition.LEFT } }, this._configurationService, this._hoverService); + store.add(this._hoverService.setupManagedHover(hoverDelegate, actionElement, labelForHoverVerbosityAction(this._keybindingService, action))); + if (!actionEnabled) { + actionElement.classList.add('disabled'); + return store; + } + actionElement.classList.add('enabled'); + const actionFunction = () => this._commandService.executeCommand(action === HoverVerbosityAction.Increase ? INCREASE_HOVER_VERBOSITY_ACTION_ID : DECREASE_HOVER_VERBOSITY_ACTION_ID); + store.add(new ClickAction(actionElement, actionFunction)); + store.add(new KeyDownAction(actionElement, actionFunction, [KeyCode.Enter, KeyCode.Space])); + return store; + } + + public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number, focus: boolean = true): Promise<{ hoverPart: MarkdownHover; hoverElement: HTMLElement } | undefined> { + const model = this._editor.getModel(); + if (!model) { + return undefined; + } + const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); + const hoverSource = hoverRenderedPart?.hoverPart.source; + if (!hoverRenderedPart || !hoverSource?.supportsVerbosityAction(action)) { + return undefined; + } + const newHover = await this._fetchHover(hoverSource, model, action); + if (!newHover) { + return undefined; + } + const newHoverSource = new HoverSource(newHover, hoverSource.hoverProvider, hoverSource.hoverPosition); + const initialHoverPart = hoverRenderedPart.hoverPart; + const newHoverPart = new MarkdownHover( + this._hoverParticipant, + initialHoverPart.range, + newHover.contents, + initialHoverPart.isBeforeContent, + initialHoverPart.ordinal, + newHoverSource + ); + const newHoverRenderedPart = this._renderHoverPart( + newHoverPart, + this._onFinishedRendering + ); + this._replaceRenderedHoverPartAtIndex(index, newHoverRenderedPart, newHoverPart); + if (focus) { + this._focusOnHoverPartWithIndex(index); + } + return { + hoverPart: newHoverPart, + hoverElement: newHoverRenderedPart.hoverElement + }; + } + + public getAccessibleContent(hoverPart: MarkdownHover): string | undefined { + const renderedHoverPartIndex = this.renderedHoverParts.findIndex(renderedHoverPart => renderedHoverPart.hoverPart === hoverPart); + if (renderedHoverPartIndex === -1) { + return undefined; + } + const renderedHoverPart = this._getRenderedHoverPartAtIndex(renderedHoverPartIndex); + if (!renderedHoverPart) { + return undefined; + } + const hoverElementInnerText = renderedHoverPart.hoverElement.innerText; + const accessibleContent = hoverElementInnerText.replace(/[^\S\n\r]+/gu, ' '); + return accessibleContent; + } + + public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); + const hoverSource = hoverRenderedPart?.hoverPart.source; + if (!hoverRenderedPart || !hoverSource?.supportsVerbosityAction(action)) { + return false; + } + return true; + } + + private async _fetchHover(hoverSource: HoverSource, model: ITextModel, action: HoverVerbosityAction): Promise { + let verbosityDelta = action === HoverVerbosityAction.Increase ? 1 : -1; + const provider = hoverSource.hoverProvider; + const ongoingHoverOperation = this._ongoingHoverOperations.get(provider); + if (ongoingHoverOperation) { + ongoingHoverOperation.tokenSource.cancel(); + verbosityDelta += ongoingHoverOperation.verbosityDelta; + } + const tokenSource = new CancellationTokenSource(); + this._ongoingHoverOperations.set(provider, { verbosityDelta, tokenSource }); + const context: HoverContext = { verbosityRequest: { verbosityDelta, previousHover: hoverSource.hover } }; + let hover: Hover | null | undefined; + try { + hover = await Promise.resolve(provider.provideHover(model, hoverSource.hoverPosition, tokenSource.token, context)); + } catch (e) { + onUnexpectedExternalError(e); + } + tokenSource.dispose(); + this._ongoingHoverOperations.delete(provider); + return hover; + } + + private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedMarkdownHoverPart, hoverPart: MarkdownHover): void { + if (index >= this.renderedHoverParts.length || index < 0) { + return; + } + const currentRenderedHoverPart = this.renderedHoverParts[index]; + const currentRenderedMarkdown = currentRenderedHoverPart.hoverElement; + const renderedMarkdown = renderedHoverPart.hoverElement; + const renderedChildrenElements = Array.from(renderedMarkdown.children); + currentRenderedMarkdown.replaceChildren(...renderedChildrenElements); + const newRenderedHoverPart = new RenderedMarkdownHoverPart( + hoverPart, + currentRenderedMarkdown, + renderedHoverPart.disposables + ); + currentRenderedMarkdown.focus(); + currentRenderedHoverPart.dispose(); + this.renderedHoverParts[index] = newRenderedHoverPart; + } + + private _focusOnHoverPartWithIndex(index: number): void { + this.renderedHoverParts[index].hoverElement.focus(); + } + + private _getRenderedHoverPartAtIndex(index: number): RenderedMarkdownHoverPart | undefined { + return this.renderedHoverParts[index]; + } + + public dispose(): void { + this._disposables.dispose(); } } export function renderMarkdownHovers( context: IEditorHoverRenderContext, - hoverParts: MarkdownHover[], + markdownHovers: MarkdownHover[], editor: ICodeEditor, languageService: ILanguageService, openerService: IOpenerService, -): IDisposable { +): IRenderedHoverParts { // Sort hover parts to keep them stable since they might come in async, out-of-order - hoverParts.sort((a, b) => a.ordinal - b.ordinal); + markdownHovers.sort(compareBy(hover => hover.ordinal, numberComparator)); + const renderedHoverParts: IRenderedHoverPart[] = []; + for (const markdownHover of markdownHovers) { + renderedHoverParts.push(renderMarkdownInContainer( + editor, + markdownHover, + languageService, + openerService, + context.onContentsChanged, + )); + } + return new RenderedHoverParts(renderedHoverParts); +} +function renderMarkdownInContainer( + editor: ICodeEditor, + markdownHover: MarkdownHover, + languageService: ILanguageService, + openerService: IOpenerService, + onFinishedRendering: () => void, +): IRenderedHoverPart { const disposables = new DisposableStore(); - for (const hoverPart of hoverParts) { - for (const contents of hoverPart.contents) { - if (isEmptyMarkdownString(contents)) { - continue; - } - const markdownHoverElement = $('div.hover-row.markdown-hover'); - const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); - const renderer = disposables.add(new MarkdownRenderer({ editor }, languageService, openerService)); - disposables.add(renderer.onDidRenderAsync(() => { - hoverContentsElement.className = 'hover-contents code-hover-contents'; - context.onContentsChanged(); - })); - const renderedContents = disposables.add(renderer.render(contents)); - hoverContentsElement.appendChild(renderedContents.element); - context.fragment.appendChild(markdownHoverElement); + const renderedMarkdown = $('div.hover-row'); + const renderedMarkdownContents = $('div.hover-row-contents'); + renderedMarkdown.appendChild(renderedMarkdownContents); + const markdownStrings = markdownHover.contents; + for (const markdownString of markdownStrings) { + if (isEmptyMarkdownString(markdownString)) { + continue; + } + const markdownHoverElement = $('div.markdown-hover'); + const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); + const renderer = disposables.add(new MarkdownRenderer({ editor }, languageService, openerService)); + disposables.add(renderer.onDidRenderAsync(() => { + hoverContentsElement.className = 'hover-contents code-hover-contents'; + onFinishedRendering(); + })); + const renderedContents = disposables.add(renderer.render(markdownString)); + hoverContentsElement.appendChild(renderedContents.element); + renderedMarkdownContents.appendChild(markdownHoverElement); + } + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: markdownHover, + hoverElement: renderedMarkdown, + dispose() { disposables.dispose(); } + }; + return renderedHoverPart; +} + +export function labelForHoverVerbosityAction(keybindingService: IKeybindingService, action: HoverVerbosityAction): string { + switch (action) { + case HoverVerbosityAction.Increase: { + const kb = keybindingService.lookupKeybinding(INCREASE_HOVER_VERBOSITY_ACTION_ID); + return kb ? + nls.localize('increaseVerbosityWithKb', "Increase Hover Verbosity ({0})", kb.getLabel()) : + nls.localize('increaseVerbosity', "Increase Hover Verbosity"); + } + case HoverVerbosityAction.Decrease: { + const kb = keybindingService.lookupKeybinding(DECREASE_HOVER_VERBOSITY_ACTION_ID); + return kb ? + nls.localize('decreaseVerbosityWithKb', "Decrease Hover Verbosity ({0})", kb.getLabel()) : + nls.localize('decreaseVerbosity', "Decrease Hover Verbosity"); } } - return disposables; } diff --git a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts index ffdc5ccf50f..033b85ce217 100644 --- a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -20,7 +20,7 @@ import { getCodeActions, quickFixCommandId } from 'vs/editor/contrib/codeAction/ import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; import { CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/browser/gotoError'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import * as nls from 'vs/nls'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; @@ -90,18 +90,28 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { if (!hoverParts.length) { - return Disposable.None; + return new RenderedHoverParts([]); } const disposables = new DisposableStore(); - hoverParts.forEach(msg => context.fragment.appendChild(this.renderMarkerHover(msg, disposables))); + const renderedHoverParts: IRenderedHoverPart[] = []; + hoverParts.forEach(hoverPart => { + const renderedMarkerHover = this._renderMarkerHover(hoverPart); + context.fragment.appendChild(renderedMarkerHover.hoverElement); + renderedHoverParts.push(renderedMarkerHover); + }); const markerHoverForStatusbar = hoverParts.length === 1 ? hoverParts[0] : hoverParts.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; this.renderMarkerStatusbar(context, markerHoverForStatusbar, disposables); - return disposables; + return new RenderedHoverParts(renderedHoverParts); } - private renderMarkerHover(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + public getAccessibleContent(hoverPart: MarkerHover): string { + return hoverPart.marker.message; + } + + private _renderMarkerHover(markerHover: MarkerHover): IRenderedHoverPart { + const disposables: DisposableStore = new DisposableStore(); const hoverElement = $('div.hover-row'); const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); const { source, message, code, relatedInformation } = markerHover.marker; @@ -153,9 +163,10 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant{ selection: { startLineNumber, startColumn } } + editorOptions }).catch(onUnexpectedError); } })); @@ -165,7 +176,12 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant = { + hoverPart: markerHover, + hoverElement, + dispose: () => disposables.dispose() + }; + return renderedHoverPart; } private renderMarkerStatusbar(context: IEditorHoverRenderContext, markerHover: MarkerHover, disposables: DisposableStore): void { diff --git a/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts b/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts index cf05d439031..65762f5905b 100644 --- a/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts +++ b/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHover'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; import { IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { TestCodeEditorInstantiationOptions, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; @@ -18,7 +18,7 @@ suite('Content Hover', () => { test('issue #151235: Gitlens hover shows up in the wrong place', () => { const text = 'just some text'; withTestCodeEditor(text, {}, (editor) => { - const actual = ContentHoverController.computeHoverRanges( + const actual = RenderedContentHover.computeHoverPositions( editor, new Range(5, 5, 5, 5), [{ range: new Range(4, 1, 5, 6) }] @@ -27,8 +27,7 @@ suite('Content Hover', () => { actual, { showAtPosition: new Position(5, 5), - showAtSecondaryPosition: new Position(5, 5), - highlightRange: new Range(4, 1, 5, 6) + showAtSecondaryPosition: new Position(5, 5) } ); }); @@ -38,7 +37,7 @@ suite('Content Hover', () => { const text = 'just some text'; const opts: TestCodeEditorInstantiationOptions = { wordWrap: 'wordWrapColumn', wordWrapColumn: 6 }; withTestCodeEditor(text, opts, (editor) => { - const actual = ContentHoverController.computeHoverRanges( + const actual = RenderedContentHover.computeHoverPositions( editor, new Range(1, 8, 1, 8), [{ range: new Range(1, 1, 1, 15) }] @@ -47,8 +46,7 @@ suite('Content Hover', () => { actual, { showAtPosition: new Position(1, 8), - showAtSecondaryPosition: new Position(1, 6), - highlightRange: new Range(1, 1, 1, 15) + showAtSecondaryPosition: new Position(1, 6) } ); }); diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index 1db0b5adb34..84760fdb6f4 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -25,6 +25,8 @@ import * as nls from 'vs/nls'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { getGoodIndentForLine, getIndentMetadata } from 'vs/editor/common/languages/autoIndent'; import { getReindentEditOperations } from '../common/indentation'; +import { getStandardTokenTypeAtPosition } from 'vs/editor/common/tokens/lineTokens'; +import { Position } from 'vs/editor/common/core/position'; export class IndentationToSpacesAction extends EditorAction { public static readonly ID = 'editor.action.indentationToSpaces'; @@ -34,7 +36,10 @@ export class IndentationToSpacesAction extends EditorAction { id: IndentationToSpacesAction.ID, label: nls.localize('indentationToSpaces', "Convert Indentation to Spaces"), alias: 'Convert Indentation to Spaces', - precondition: EditorContextKeys.writable + precondition: EditorContextKeys.writable, + metadata: { + description: nls.localize2('indentationToSpacesDescription', "Convert the tab indentation to spaces."), + } }); } @@ -68,7 +73,10 @@ export class IndentationToTabsAction extends EditorAction { id: IndentationToTabsAction.ID, label: nls.localize('indentationToTabs', "Convert Indentation to Tabs"), alias: 'Convert Indentation to Tabs', - precondition: EditorContextKeys.writable + precondition: EditorContextKeys.writable, + metadata: { + description: nls.localize2('indentationToTabsDescription', "Convert the spaces indentation to tabs."), + } }); } @@ -161,7 +169,10 @@ export class IndentUsingTabs extends ChangeIndentationSizeAction { id: IndentUsingTabs.ID, label: nls.localize('indentUsingTabs', "Indent Using Tabs"), alias: 'Indent Using Tabs', - precondition: undefined + precondition: undefined, + metadata: { + description: nls.localize2('indentUsingTabsDescription', "Use indentation with tabs."), + } }); } } @@ -175,7 +186,10 @@ export class IndentUsingSpaces extends ChangeIndentationSizeAction { id: IndentUsingSpaces.ID, label: nls.localize('indentUsingSpaces', "Indent Using Spaces"), alias: 'Indent Using Spaces', - precondition: undefined + precondition: undefined, + metadata: { + description: nls.localize2('indentUsingSpacesDescription', "Use indentation with spaces."), + } }); } } @@ -189,7 +203,10 @@ export class ChangeTabDisplaySize extends ChangeIndentationSizeAction { id: ChangeTabDisplaySize.ID, label: nls.localize('changeTabDisplaySize', "Change Tab Display Size"), alias: 'Change Tab Display Size', - precondition: undefined + precondition: undefined, + metadata: { + description: nls.localize2('changeTabDisplaySizeDescription', "Change the space size equivalent of the tab."), + } }); } } @@ -203,7 +220,10 @@ export class DetectIndentation extends EditorAction { id: DetectIndentation.ID, label: nls.localize('detectIndentation', "Detect Indentation from Content"), alias: 'Detect Indentation from Content', - precondition: undefined + precondition: undefined, + metadata: { + description: nls.localize2('detectIndentationDescription', "Detect the indentation from content."), + } }); } @@ -226,7 +246,10 @@ export class ReindentLinesAction extends EditorAction { id: 'editor.action.reindentlines', label: nls.localize('editor.reindentlines', "Reindent Lines"), alias: 'Reindent Lines', - precondition: EditorContextKeys.writable + precondition: EditorContextKeys.writable, + metadata: { + description: nls.localize2('editor.reindentlinesDescription', "Reindent the lines of the editor."), + } }); } @@ -252,7 +275,10 @@ export class ReindentSelectedLinesAction extends EditorAction { id: 'editor.action.reindentselectedlines', label: nls.localize('editor.reindentselectedlines', "Reindent Selected Lines"), alias: 'Reindent Selected Lines', - precondition: EditorContextKeys.writable + precondition: EditorContextKeys.writable, + metadata: { + description: nls.localize2('editor.reindentselectedlinesDescription', "Reindent the selected lines of the editor."), + } }); } @@ -392,7 +418,13 @@ export class AutoIndentOnPaste implements IEditorContribution { if (!model) { return; } - + const containsOnlyWhitespace = this.rangeContainsOnlyWhitespaceCharacters(model, range); + if (containsOnlyWhitespace) { + return; + } + if (isStartOrEndInString(model, range)) { + return; + } if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber)) { return; } @@ -438,7 +470,7 @@ export class AutoIndentOnPaste implements IEditorContribution { range: new Range(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), text: newIndent }); - firstLineText = newIndent + firstLineText.substr(oldIndentation.length); + firstLineText = newIndent + firstLineText.substring(oldIndentation.length); } else { const indentMetadata = getIndentMetadata(model, startLineNumber, this._languageConfigurationService); @@ -518,6 +550,35 @@ export class AutoIndentOnPaste implements IEditorContribution { } } + private rangeContainsOnlyWhitespaceCharacters(model: ITextModel, range: Range): boolean { + const lineContainsOnlyWhitespace = (content: string): boolean => { + return content.trim().length === 0; + }; + let containsOnlyWhitespace: boolean = true; + if (range.startLineNumber === range.endLineNumber) { + const lineContent = model.getLineContent(range.startLineNumber); + const linePart = lineContent.substring(range.startColumn - 1, range.endColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else { + for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { + const lineContent = model.getLineContent(i); + if (i === range.startLineNumber) { + const linePart = lineContent.substring(range.startColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else if (i === range.endLineNumber) { + const linePart = lineContent.substring(0, range.endColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else { + containsOnlyWhitespace = model.getLineFirstNonWhitespaceColumn(i) === 0; + } + if (!containsOnlyWhitespace) { + break; + } + } + } + return containsOnlyWhitespace; + } + private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { model.tokenization.forceTokenization(lineNumber); const nonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); @@ -541,6 +602,14 @@ export class AutoIndentOnPaste implements IEditorContribution { } } +function isStartOrEndInString(model: ITextModel, range: Range): boolean { + const isPositionInString = (position: Position): boolean => { + const tokenType = getStandardTokenTypeAtPosition(model, position); + return tokenType === StandardTokenType.String; + }; + return isPositionInString(range.getStartPosition()) || isPositionInString(range.getEndPosition()); +} + function getIndentationEditOperations(model: ITextModel, builder: IEditOperationBuilder, tabSize: number, tabsToSpaces: boolean): void { if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { // Model is empty diff --git a/src/vs/editor/contrib/indentation/common/indentation.ts b/src/vs/editor/contrib/indentation/common/indentation.ts index 760a14919fb..599d8012e0f 100644 --- a/src/vs/editor/contrib/indentation/common/indentation.ts +++ b/src/vs/editor/contrib/indentation/common/indentation.ts @@ -8,30 +8,28 @@ import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { normalizeIndentation } from 'vs/editor/common/core/indentation'; import { Selection } from 'vs/editor/common/core/selection'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; import { ITextModel } from 'vs/editor/common/model'; -export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] { +export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number): ISingleEditOperation[] { if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { // Model is empty return []; } - const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules; - if (!indentationRules) { + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; + if (!indentationRulesSupport) { return []; } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); endLineNumber = Math.min(endLineNumber, model.getLineCount()); // Skip `unIndentedLinePattern` lines while (startLineNumber <= endLineNumber) { - if (!indentationRules.unIndentedLinePattern) { - break; - } - - const text = model.getLineContent(startLineNumber); - if (!indentationRules.unIndentedLinePattern.test(text)) { + if (!processedIndentRulesSupport.shouldIgnore(startLineNumber)) { break; } @@ -54,37 +52,19 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati const indentEdits: ISingleEditOperation[] = []; // indentation being passed to lines below - let globalIndent: string; // Calculate indentation for the first line // If there is no passed-in indentation, we use the indentation of the first line as base. const currentLineText = model.getLineContent(startLineNumber); - let adjustedLineContent = currentLineText; - if (inheritedIndent !== undefined && inheritedIndent !== null) { - globalIndent = inheritedIndent; - const oldIndentation = strings.getLeadingWhitespace(currentLineText); - - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { - globalIndent = unshiftIndent(globalIndent); - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - - } - if (currentLineText !== adjustedLineContent) { - indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces))); - } - } else { - globalIndent = strings.getLeadingWhitespace(currentLineText); - } - + let globalIndent = strings.getLeadingWhitespace(currentLineText); // idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`. let idealIndentForNextLine: string = globalIndent; - if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + if (processedIndentRulesSupport.shouldIncrease(startLineNumber)) { idealIndentForNextLine = shiftIndent(idealIndentForNextLine); globalIndent = shiftIndent(globalIndent); } - else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + else if (processedIndentRulesSupport.shouldIndentNextLine(startLineNumber)) { idealIndentForNextLine = shiftIndent(idealIndentForNextLine); } @@ -92,11 +72,14 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati // Calculate indentation adjustment for all following lines for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + if (doesLineStartWithString(model, lineNumber)) { + continue; + } const text = model.getLineContent(lineNumber); const oldIndentation = strings.getLeadingWhitespace(text); - const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length); + const currentIdealIndent = idealIndentForNextLine; - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { + if (processedIndentRulesSupport.shouldDecrease(lineNumber, currentIdealIndent)) { idealIndentForNextLine = unshiftIndent(idealIndentForNextLine); globalIndent = unshiftIndent(globalIndent); } @@ -106,14 +89,14 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati } // calculate idealIndentForNextLine - if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) { + if (processedIndentRulesSupport.shouldIgnore(lineNumber)) { // In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines // but don't change globalIndent and idealIndentForNextLine. continue; - } else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + } else if (processedIndentRulesSupport.shouldIncrease(lineNumber, currentIdealIndent)) { globalIndent = shiftIndent(globalIndent); idealIndentForNextLine = globalIndent; - } else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + } else if (processedIndentRulesSupport.shouldIndentNextLine(lineNumber, currentIdealIndent)) { idealIndentForNextLine = shiftIndent(idealIndentForNextLine); } else { idealIndentForNextLine = globalIndent; @@ -122,3 +105,11 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati return indentEdits; } + +function doesLineStartWithString(model: ITextModel, lineNumber: number): boolean { + if (!model.tokenization.isCheapToTokenize(lineNumber)) { + return false; + } + const lineTokens = model.tokenization.getLineTokens(lineNumber); + return lineTokens.getStandardTokenType(0) === StandardTokenType.String; +} diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 6f125be77d6..2beed4f823b 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -3,30 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import assert from 'assert'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; +import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { AutoIndentOnPaste, IndentationToSpacesCommand, IndentationToTabsCommand } from 'vs/editor/contrib/indentation/browser/indentation'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { testCommand } from 'vs/editor/test/browser/testCommand'; -import { goIndentationRules, javascriptIndentationRules, phpIndentationRules, rubyIndentationRules } from 'vs/editor/test/common/modes/supports/indentationRules'; -import { cppOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; +import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, latexIndentationRules, luaIndentationRules, phpIndentationRules, rubyIndentationRules } from 'vs/editor/test/common/modes/supports/indentationRules'; +import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; +import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { cppBracketRules, goBracketRules, htmlBracketRules, latexBracketRules, luaBracketRules, phpBracketRules, rubyBracketRules, typescriptBracketRules, vbBracketRules } from 'vs/editor/test/common/modes/supports/bracketRules'; +import { javascriptAutoClosingPairsRules, latexAutoClosingPairsRules } from 'vs/editor/test/common/modes/supports/autoClosingPairsRules'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; -enum Language { - TypeScript, - Ruby, - PHP, - Go, - CPP +export enum Language { + TypeScript = 'ts-test', + Ruby = 'ruby-test', + PHP = 'php-test', + Go = 'go-test', + CPP = 'cpp-test', + HTML = 'html-test', + VB = 'vb-test', + Latex = 'latex-test', + Lua = 'lua-test' } function testIndentationToSpacesCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void { @@ -37,77 +47,74 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } -function registerLanguage(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) { - const languageService = instantiationService.get(ILanguageService); - registerLanguageConfiguration(instantiationService, languageId, language, disposables); - disposables.add(languageService.registerLanguage({ id: languageId })); +export function registerLanguage(languageService: ILanguageService, language: Language): IDisposable { + return languageService.registerLanguage({ id: language }); } -// TODO@aiday-mar read directly the configuration file -function registerLanguageConfiguration(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) { - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); +export function registerLanguageConfiguration(languageConfigurationService: ILanguageConfigurationService, language: Language): IDisposable { switch (language) { case Language.TypeScript: - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['${', '}'], - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], + return languageConfigurationService.register(language, { + brackets: typescriptBracketRules, comments: { lineComment: '//', blockComment: ['/*', '*/'] }, + autoClosingPairs: javascriptAutoClosingPairsRules, indentationRules: javascriptIndentationRules, onEnterRules: javascriptOnEnterRules - })); - break; + }); case Language.Ruby: - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], + return languageConfigurationService.register(language, { + brackets: rubyBracketRules, indentationRules: rubyIndentationRules, - })); - break; + }); case Language.PHP: - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], + return languageConfigurationService.register(language, { + brackets: phpBracketRules, indentationRules: phpIndentationRules, onEnterRules: phpOnEnterRules - })); - break; + }); case Language.Go: - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], + return languageConfigurationService.register(language, { + brackets: goBracketRules, indentationRules: goIndentationRules - })); - break; + }); case Language.CPP: - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], + return languageConfigurationService.register(language, { + brackets: cppBracketRules, onEnterRules: cppOnEnterRules - })); - break; + }); + case Language.HTML: + return languageConfigurationService.register(language, { + brackets: htmlBracketRules, + indentationRules: htmlIndentationRules, + onEnterRules: htmlOnEnterRules + }); + case Language.VB: + return languageConfigurationService.register(language, { + brackets: vbBracketRules, + }); + case Language.Latex: + return languageConfigurationService.register(language, { + brackets: latexBracketRules, + autoClosingPairs: latexAutoClosingPairsRules, + indentationRules: latexIndentationRules + }); + case Language.Lua: + return languageConfigurationService.register(language, { + brackets: luaBracketRules, + indentationRules: luaIndentationRules + }); } } -function registerTokens(instantiationService: TestInstantiationService, tokens: { startIndex: number; value: number }[][], languageId: string, disposables: DisposableStore) { +export interface StandardTokenTypeData { + startIndex: number; + standardTokenType: StandardTokenType; +} + +export function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: Language): IDisposable { let lineIndex = 0; const languageService = instantiationService.get(ILanguageService); const tokenizationSupport: ITokenizationSupport = { @@ -122,13 +129,13 @@ function registerTokens(instantiationService: TestInstantiationService, tokens: result[2 * i + 1] = ( (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) - | (tokensOnLine[i].value << MetadataConsts.TOKEN_TYPE_OFFSET) + | (tokensOnLine[i].standardTokenType << MetadataConsts.TOKEN_TYPE_OFFSET) ); } return new EncodedTokenizationResult(result, state); } }; - disposables.add(TokenizationRegistry.register(languageId, tokenizationSupport)); + return TokenizationRegistry.register(languageId, tokenizationSupport); } suite('Change Indentation to Spaces - TypeScript/Javascript', () => { @@ -305,13 +312,103 @@ suite('Change Indentation to Tabs - TypeScript/Javascript', () => { }); }); -suite('Auto Indent On Paste - TypeScript/JavaScript', () => { +suite('Indent With Tab - TypeScript/JavaScript', () => { - const languageId = 'ts-test'; + const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #63388: perserve correct indentation on tab 1', () => { + + // https://github.com/microsoft/vscode/issues/63388 + + const model = createTextModel([ + '/*', + ' * Comment', + ' * /', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(1, 1, 3, 5)); + editor.executeCommands('editor.action.indentLines', TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + assert.strictEqual(model.getValue(), [ + ' /*', + ' * Comment', + ' * /', + ].join('\n')); + }); + }); + + test.skip('issue #63388: perserve correct indentation on tab 2', () => { + + // https://github.com/microsoft/vscode/issues/63388 + + const model = createTextModel([ + 'switch (something) {', + ' case 1:', + ' whatever();', + ' break;', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(1, 1, 5, 2)); + editor.executeCommands('editor.action.indentLines', TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + assert.strictEqual(model.getValue(), [ + ' switch (something) {', + ' case 1:', + ' whatever();', + ' break;', + ' }', + ].join('\n')); + }); + }); +}); + +suite('Auto Indent On Paste - TypeScript/JavaScript', () => { + + const languageId = Language.TypeScript; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -325,42 +422,41 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const pasteText = [ '/**', ' * JSDoc', ' */', 'function a() {}' ].join('\n'); - const tokens = [ + const tokens: StandardTokenTypeData[][] = [ [ - { startIndex: 0, value: 1 }, - { startIndex: 3, value: 1 }, + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 3, standardTokenType: StandardTokenType.Comment }, ], [ - { startIndex: 0, value: 1 }, - { startIndex: 2, value: 1 }, - { startIndex: 8, value: 1 }, + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 2, standardTokenType: StandardTokenType.Comment }, + { startIndex: 8, standardTokenType: StandardTokenType.Comment }, ], [ - { startIndex: 0, value: 1 }, - { startIndex: 1, value: 1 }, - { startIndex: 3, value: 0 }, + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 1, standardTokenType: StandardTokenType.Comment }, + { startIndex: 3, standardTokenType: StandardTokenType.Other }, ], [ - { startIndex: 0, value: 0 }, - { startIndex: 8, value: 0 }, - { startIndex: 9, value: 0 }, - { startIndex: 10, value: 0 }, - { startIndex: 11, value: 0 }, - { startIndex: 12, value: 0 }, - { startIndex: 13, value: 0 }, - { startIndex: 14, value: 0 }, - { startIndex: 15, value: 0 }, + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 8, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.Other }, + { startIndex: 10, standardTokenType: StandardTokenType.Other }, + { startIndex: 11, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.Other }, + { startIndex: 13, standardTokenType: StandardTokenType.Other }, + { startIndex: 14, standardTokenType: StandardTokenType.Other }, + { startIndex: 15, standardTokenType: StandardTokenType.Other }, ] ]; - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); - registerTokens(instantiationService, tokens, languageId, disposables); + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 4, 16)); @@ -373,7 +469,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { // no need for tokenization because there are no comments const pasteText = [ @@ -390,7 +486,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}' ].join('\n'); - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); @@ -408,8 +503,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 6, 2, 6)); const text = ', null'; viewModel.paste(text, true, undefined, 'keyboard'); @@ -436,8 +530,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(5, 24, 5, 34)); const text = 'IMacLinuxKeyMapping'; viewModel.paste(text, true, undefined, 'keyboard'); @@ -461,8 +554,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel('', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const text = [ '/*----------------', ' * Copyright (c) ', @@ -476,6 +568,42 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { }); }); + test('issue #209859: do not do change indentation when pasted inside of a string', () => { + + // issue: https://github.com/microsoft/vscode/issues/209859 + // issue: https://github.com/microsoft/vscode/issues/209418 + + const initialText = [ + 'const foo = "some text', + ' which is strangely', + ' indented"' + ].join('\n'); + const model = createTextModel(initialText, languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + + editor.setSelection(new Selection(2, 10, 2, 15)); + viewModel.paste('which', true, undefined, 'keyboard'); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + autoIndentOnPasteController.trigger(new Range(2, 1, 2, 28)); + assert.strictEqual(model.getValue(), initialText); + }); + }); + // Failing tests found in issues... test.skip('issue #181065: Incorrect paste of object within comment', () => { @@ -485,40 +613,39 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const text = [ '/**', ' * @typedef {', ' * }', ' */' ].join('\n'); - const tokens = [ + const tokens: StandardTokenTypeData[][] = [ [ - { startIndex: 0, value: 1 }, - { startIndex: 3, value: 1 }, + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 3, standardTokenType: StandardTokenType.Comment }, ], [ - { startIndex: 0, value: 1 }, - { startIndex: 2, value: 1 }, - { startIndex: 3, value: 1 }, - { startIndex: 11, value: 1 }, - { startIndex: 12, value: 0 }, - { startIndex: 13, value: 0 }, + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 2, standardTokenType: StandardTokenType.Comment }, + { startIndex: 3, standardTokenType: StandardTokenType.Comment }, + { startIndex: 11, standardTokenType: StandardTokenType.Comment }, + { startIndex: 12, standardTokenType: StandardTokenType.Other }, + { startIndex: 13, standardTokenType: StandardTokenType.Other }, ], [ - { startIndex: 0, value: 1 }, - { startIndex: 2, value: 0 }, - { startIndex: 3, value: 0 }, - { startIndex: 4, value: 0 }, + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 2, standardTokenType: StandardTokenType.Other }, + { startIndex: 3, standardTokenType: StandardTokenType.Other }, + { startIndex: 4, standardTokenType: StandardTokenType.Other }, ], [ - { startIndex: 0, value: 1 }, - { startIndex: 1, value: 1 }, - { startIndex: 3, value: 0 }, + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 1, standardTokenType: StandardTokenType.Comment }, + { startIndex: 3, standardTokenType: StandardTokenType.Other }, ] ]; - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); - registerTokens(instantiationService, tokens, languageId, disposables); + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 4, 4)); @@ -537,7 +664,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 1, 2, 1)); const text = [ '() => {', @@ -545,7 +672,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}', '' ].join('\n'); - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(2, 1, 5, 1)); @@ -579,7 +705,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 5, 2, 5)); const text = [ '() => {', @@ -587,7 +713,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}', ' ' ].join('\n'); - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); // todo@aiday-mar, make sure range is correct, and make test work as in real life @@ -610,7 +735,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel('', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 5, 2, 5)); const text = [ 'function makeSub(a,b) {', @@ -618,15 +743,14 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { 'return subsent;', '}', ].join('\n'); - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); // todo@aiday-mar, make sure range is correct, and make test work as in real life autoIndentOnPasteController.trigger(new Range(1, 1, 4, 2)); assert.strictEqual(model.getValue(), [ 'function makeSub(a,b) {', - ' subsent = sent.substring(a,b);', - ' return subsent;', + 'subsent = sent.substring(a,b);', + 'return subsent;', '}', ].join('\n')); }); @@ -643,22 +767,44 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - const tokens = [ - [{ startIndex: 0, value: 0 }, { startIndex: 8, value: 0 }, { startIndex: 9, value: 0 }, { startIndex: 12, value: 0 }, { startIndex: 13, value: 0 }, { startIndex: 14, value: 0 }, { startIndex: 15, value: 0 }, { startIndex: 16, value: 0 }], - [{ startIndex: 0, value: 1 }, { startIndex: 2, value: 1 }, { startIndex: 3, value: 1 }, { startIndex: 10, value: 1 }], - [{ startIndex: 0, value: 0 }, { startIndex: 5, value: 0 }, { startIndex: 6, value: 0 }, { startIndex: 9, value: 0 }, { startIndex: 10, value: 0 }, { startIndex: 11, value: 0 }, { startIndex: 12, value: 0 }, { startIndex: 14, value: 0 }], - [{ startIndex: 0, value: 0 }, { startIndex: 1, value: 0 }] + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 8, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.Other }, + { startIndex: 13, standardTokenType: StandardTokenType.Other }, + { startIndex: 14, standardTokenType: StandardTokenType.Other }, + { startIndex: 15, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Comment }, + { startIndex: 2, standardTokenType: StandardTokenType.Comment }, + { startIndex: 3, standardTokenType: StandardTokenType.Comment }, + { startIndex: 10, standardTokenType: StandardTokenType.Comment } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 5, standardTokenType: StandardTokenType.Other }, + { startIndex: 6, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.Other }, + { startIndex: 10, standardTokenType: StandardTokenType.Other }, + { startIndex: 11, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.Other }, + { startIndex: 14, standardTokenType: StandardTokenType.Other }], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 1, standardTokenType: StandardTokenType.Other }] ]; - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); - registerTokens(instantiationService, tokens, languageId, disposables); + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(2, 1, 2, 1)); const text = [ '// comment', 'const foo = 42', ].join('\n'); - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(2, 1, 3, 15)); @@ -674,11 +820,22 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { suite('Auto Indent On Type - TypeScript/JavaScript', () => { - const languageId = "ts-test"; + const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -696,8 +853,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type('const add1 = (n) =>'); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -718,8 +874,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 9, 3, 9)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -738,10 +893,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type([ 'const add1 = (n) =>', ' n + 1;', @@ -767,9 +919,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 1, 3, 1)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -798,8 +948,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: 'advanced', serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(7, 6, 7, 6)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), @@ -829,8 +978,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: 'advanced', serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 4, 1, 4)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), @@ -855,8 +1003,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 12, 2, 12)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -879,9 +1026,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 19, 2, 19)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -903,9 +1048,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); - // Failing tests... - - test.skip('issue #208232: incorrect indentation inside of comments', () => { + test('issue #208232: incorrect indentation inside of comments', () => { // https://github.com/microsoft/vscode/issues/208232 @@ -916,9 +1059,13 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(2, 23, 2, 23)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -930,6 +1077,40 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); + test('issue #209802: allman style braces in JavaScript', () => { + + // https://github.com/microsoft/vscode/issues/209802 + + const model = createTextModel([ + 'if (/*condition*/)', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(1, 19, 1, 19)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + ' ' + ].join('\n')); + viewModel.type("{", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + '{}' + ].join('\n')); + editor.setSelection(new Selection(2, 2, 2, 2)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + '{', + ' ', + '}' + ].join('\n')); + }); + }); + + // Failing tests... + test.skip('issue #43244: indent after equal sign is detected', () => { // https://github.com/microsoft/vscode/issues/43244 @@ -943,8 +1124,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 14, 1, 14)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -967,8 +1147,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 7, 2, 7)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -991,8 +1170,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 7, 2, 7)); viewModel.type("\n", 'keyboard'); viewModel.type("."); @@ -1016,8 +1194,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 24, 2, 24)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1042,8 +1219,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 5, 3, 5)); viewModel.type("."); assert.strictEqual(model.getValue(), [ @@ -1066,8 +1242,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 25, 2, 25)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1086,10 +1261,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel('function foo() {}', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 17, 1, 17)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1120,9 +1292,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 14, 3, 14)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1136,20 +1306,97 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); - // Add tests for: - // https://github.com/microsoft/vscode/issues/88638 - // https://github.com/microsoft/vscode/issues/63388 - // https://github.com/microsoft/vscode/issues/46401 - // https://github.com/microsoft/vscode/issues/174044 + test.skip('issue #67678: indent on typing curly brace', () => { + + // https://github.com/microsoft/vscode/issues/67678 + + const model = createTextModel([ + 'if (true) {', + 'console.log("a")', + 'console.log("b")', + '', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 1, 4, 1)); + viewModel.type("}", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (true) {', + ' console.log("a")', + ' console.log("b")', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #46401: outdent when encountering bracket on line - allman style indentation', () => { + + // https://github.com/microsoft/vscode/issues/46401 + + const model = createTextModel([ + 'if (true)', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type("{}", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (true)', + '{}', + ].join('\n')); + editor.setSelection(new Selection(2, 2, 2, 2)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (true)', + '{', + ' ', + '}' + ].join('\n')); + }); + }); + + test.skip('issue #125261: typing closing brace does not keep the current indentation', () => { + + // https://github.com/microsoft/vscode/issues/125261 + + const model = createTextModel([ + 'foo {', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "keep", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type("}", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'foo {', + '}', + ].join('\n')); + }); + }); }); suite('Auto Indent On Type - Ruby', () => { - const languageId = "ruby-test"; + const languageId = Language.Ruby; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1165,10 +1412,7 @@ suite('Auto Indent On Type - Ruby', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.Ruby, disposables); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type("def foo\n i"); viewModel.type("n", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\n in"); @@ -1193,10 +1437,7 @@ suite('Auto Indent On Type - Ruby', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.Ruby, disposables); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type("method('#foo') do"); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1209,11 +1450,22 @@ suite('Auto Indent On Type - Ruby', () => { suite('Auto Indent On Type - PHP', () => { - const languageId = "php-test"; + const languageId = Language.PHP; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1222,24 +1474,26 @@ suite('Auto Indent On Type - PHP', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('temp issue because there should be at least one passing test in a suite', () => { - assert.ok(true); - }); - - test.skip('issue #199050: should not indent after { detected in a string', () => { + test('issue #199050: should not indent after { detected in a string', () => { // https://github.com/microsoft/vscode/issues/199050 - const model = createTextModel("$phrase = preg_replace('#(\{1|%s).*#su', '', $phrase);", languageId, {}); + const model = createTextModel("preg_replace('{');", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - registerLanguage(instantiationService, languageId, Language.PHP, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 13, standardTokenType: StandardTokenType.String }, + { startIndex: 16, standardTokenType: StandardTokenType.Other }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(1, 54, 1, 54)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ - "$phrase = preg_replace('#(\{1|%s).*#su', '', $phrase);", + "preg_replace('{');", "" ].join('\n')); }); @@ -1248,11 +1502,22 @@ suite('Auto Indent On Type - PHP', () => { suite('Auto Indent On Paste - Go', () => { - const languageId = "go-test"; + const languageId = Language.Go; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1277,8 +1542,7 @@ suite('Auto Indent On Paste - Go', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.Go, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 1, 3, 1)); const text = ' '; const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); @@ -1296,11 +1560,22 @@ suite('Auto Indent On Paste - Go', () => { suite('Auto Indent On Type - CPP', () => { - const languageId = "cpp-test"; + const languageId = Language.CPP; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1323,8 +1598,7 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - registerLanguage(instantiationService, languageId, Language.CPP, disposables); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 20, 2, 20)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1335,4 +1609,255 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n')); }); }); + + test.skip('issue #118929: incorrect indent when // follows curly brace', () => { + + // https://github.com/microsoft/vscode/issues/118929 + + const model = createTextModel([ + 'if (true) { // jaja', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(1, 20, 1, 20)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (true) { // jaja', + ' ', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #111265: auto indentation set to "none" still changes the indentation', () => { + + // https://github.com/microsoft/vscode/issues/111265 + + const model = createTextModel([ + 'int func() {', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "none", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 3, 2, 3)); + viewModel.type("}", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'int func() {', + ' }', + ].join('\n')); + }); + }); + +}); + +suite('Auto Indent On Type - HTML', () => { + + const languageId = Language.HTML; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #61510: incorrect indentation after // in html file', () => { + + // https://github.com/microsoft/vscode/issues/178334 + + const model = createTextModel([ + '
',
+			'  foo //I press  at the end of this line',
+			'
', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 48, 2, 48)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + '
',
+				'  foo //I press  at the end of this line',
+				'  ',
+				'
', + ].join('\n')); + }); + }); +}); + +suite('Auto Indent On Type - Visual Basic', () => { + + const languageId = Language.VB; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #118932: no indentation in visual basic files', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'if True then', + ' Some code', + ' end i', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(3, 10, 3, 10)); + viewModel.type("f", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if True then', + ' Some code', + 'end if', + ].join('\n')); + }); + }); +}); + + +suite('Auto Indent On Type - Latex', () => { + + const languageId = Language.Latex; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #178075: no auto closing pair when indentation done', () => { + + // https://github.com/microsoft/vscode/issues/178075 + + const model = createTextModel([ + '\\begin{theorem}', + ' \\end', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 9, 2, 9)); + viewModel.type("{", 'keyboard'); + assert.strictEqual(model.getValue(), [ + '\\begin{theorem}', + '\\end{}', + ].join('\n')); + }); + }); +}); + +suite('Auto Indent On Type - Lua', () => { + + const languageId = Language.Lua; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('temp issue because there should be at least one passing test in a suite', () => { + assert.ok(true); + }); + + test.skip('issue #178075: no auto closing pair when indentation done', () => { + + // https://github.com/microsoft/vscode/issues/178075 + + const model = createTextModel([ + 'print("asdf function asdf")', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(1, 28, 1, 28)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'print("asdf function asdf")', + '' + ].join('\n')); + }); + }); }); diff --git a/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts new file mode 100644 index 00000000000..2004b864cbe --- /dev/null +++ b/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IndentationContextProcessor, ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; +import { Language, registerLanguage, registerLanguageConfiguration, registerTokenizationSupport, StandardTokenTypeData } from 'vs/editor/contrib/indentation/test/browser/indentation.test'; +import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { Range } from 'vs/editor/common/core/range'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; + +suite('Indentation Context Processor - TypeScript/JavaScript', () => { + + const languageId = Language.TypeScript; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('brackets inside of string', () => { + + const model = createTextModel([ + 'const someVar = "{some text}"', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [[ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.String }, + { startIndex: 28, standardTokenType: StandardTokenType.String } + ]]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContext = indentationContextProcessor.getProcessedTokenContextAroundRange(new Range(1, 23, 1, 23)); + assert.strictEqual(processedContext.beforeRangeProcessedTokens.getLineContent(), 'const someVar = "some'); + assert.strictEqual(processedContext.afterRangeProcessedTokens.getLineContent(), ' text"'); + assert.strictEqual(processedContext.previousLineProcessedTokens.getLineContent(), ''); + }); + }); + + test('brackets inside of comment', () => { + + const model = createTextModel([ + 'const someVar2 = /*(a])*/', + 'const someVar = /* [()] some other t{e}xt() */ "some text"', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 17, standardTokenType: StandardTokenType.Comment }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.Comment }, + { startIndex: 46, standardTokenType: StandardTokenType.Other }, + { startIndex: 47, standardTokenType: StandardTokenType.String } + ]]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContext = indentationContextProcessor.getProcessedTokenContextAroundRange(new Range(2, 29, 2, 35)); + assert.strictEqual(processedContext.beforeRangeProcessedTokens.getLineContent(), 'const someVar = /* some'); + assert.strictEqual(processedContext.afterRangeProcessedTokens.getLineContent(), ' text */ "some text"'); + assert.strictEqual(processedContext.previousLineProcessedTokens.getLineContent(), 'const someVar2 = /*a*/'); + }); + }); + + test('brackets inside of regex', () => { + + const model = createTextModel([ + 'const someRegex2 = /(()))]/;', + 'const someRegex = /()a{h}{s}[(a}87(9a9()))]/;', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 19, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 27, standardTokenType: StandardTokenType.Other }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 18, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 44, standardTokenType: StandardTokenType.Other }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContext = indentationContextProcessor.getProcessedTokenContextAroundRange(new Range(1, 25, 2, 33)); + assert.strictEqual(processedContext.beforeRangeProcessedTokens.getLineContent(), 'const someRegex2 = /'); + assert.strictEqual(processedContext.afterRangeProcessedTokens.getLineContent(), '879a9/;'); + assert.strictEqual(processedContext.previousLineProcessedTokens.getLineContent(), ''); + }); + }); +}); + +suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { + + const languageId = Language.TypeScript; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should increase', () => { + + const model = createTextModel([ + 'const someVar = {', + 'const someVar2 = "{"', + 'const someVar3 = /*{*/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 17, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 17, standardTokenType: StandardTokenType.Comment }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; + if (!indentationRulesSupport) { + assert.fail('indentationRulesSupport should be defined'); + } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); + assert.strictEqual(processedIndentRulesSupport.shouldIncrease(1), true); + assert.strictEqual(processedIndentRulesSupport.shouldIncrease(2), false); + assert.strictEqual(processedIndentRulesSupport.shouldIncrease(3), false); + }); + }); + + test('should decrease', () => { + + const model = createTextModel([ + '}', + '"])some text}"', + '])*/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [{ startIndex: 0, standardTokenType: StandardTokenType.Other }], + [{ startIndex: 0, standardTokenType: StandardTokenType.String }], + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; + if (!indentationRulesSupport) { + assert.fail('indentationRulesSupport should be defined'); + } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); + assert.strictEqual(processedIndentRulesSupport.shouldDecrease(1), true); + assert.strictEqual(processedIndentRulesSupport.shouldDecrease(2), false); + assert.strictEqual(processedIndentRulesSupport.shouldDecrease(3), false); + }); + }); + + test('should increase next line', () => { + + const model = createTextModel([ + 'if()', + 'const someString = "if()"', + 'const someRegex = /if()/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 19, standardTokenType: StandardTokenType.String } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 18, standardTokenType: StandardTokenType.RegEx } + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; + if (!indentationRulesSupport) { + assert.fail('indentationRulesSupport should be defined'); + } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); + assert.strictEqual(processedIndentRulesSupport.shouldIndentNextLine(1), true); + assert.strictEqual(processedIndentRulesSupport.shouldIndentNextLine(2), false); + assert.strictEqual(processedIndentRulesSupport.shouldIndentNextLine(3), false); + }); + }); +}); diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index b2b665a7edd..accb88043ff 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ModifierKeyEmitter } from 'vs/base/browser/dom'; +import { isHTMLElement, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -358,7 +358,7 @@ export class InlayHintsController implements IEditorContribution { private _installContextMenu(): IDisposable { return this._editor.onContextMenu(async e => { - if (!(e.event.target instanceof HTMLElement)) { + if (!(isHTMLElement(e.event.target))) { return; } const part = this._getInlayHintLabelPart(e); diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts index 86d160b2782..05703e108d1 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts @@ -13,7 +13,7 @@ import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textM import { HoverAnchor, HoverForeignElementAnchor, IEditorHoverParticipant } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { getHover } from 'vs/editor/contrib/hover/browser/getHover'; +import { getHoverProviderResultsAsAsyncIterable } from 'vs/editor/contrib/hover/browser/getHover'; import { MarkdownHover, MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; import { RenderedInlayHintLabelPart, InlayHintsController } from 'vs/editor/contrib/inlayHints/browser/inlayHintsController'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -24,6 +24,9 @@ import { localize } from 'vs/nls'; import * as platform from 'vs/base/common/platform'; import { asCommandLink } from 'vs/editor/contrib/inlayHints/browser/inlayHints'; import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { ICommandService } from 'vs/platform/commands/common/commands'; class InlayHintsHoverAnchor extends HoverForeignElementAnchor { constructor( @@ -44,11 +47,14 @@ export class InlayHintsHover extends MarkdownHoverParticipant implements IEditor editor: ICodeEditor, @ILanguageService languageService: ILanguageService, @IOpenerService openerService: IOpenerService, + @IKeybindingService keybindingService: IKeybindingService, + @IHoverService hoverService: IHoverService, @IConfigurationService configurationService: IConfigurationService, @ITextModelService private readonly _resolverService: ITextModelService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @ICommandService commandService: ICommandService ) { - super(editor, languageService, openerService, configurationService, languageFeaturesService); + super(editor, languageService, openerService, configurationService, languageFeaturesService, keybindingService, hoverService, commandService); } suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null { @@ -154,7 +160,7 @@ export class InlayHintsHover extends MarkdownHoverParticipant implements IEditor if (!this._languageFeaturesService.hoverProvider.has(model)) { return AsyncIterableObject.EMPTY; } - return getHover(this._languageFeaturesService.hoverProvider, model, new Position(range.startLineNumber, range.startColumn), token) + return getHoverProviderResultsAsAsyncIterable(this._languageFeaturesService.hoverProvider, model, new Position(range.startLineNumber, range.startColumn), token) .filter(item => !isEmptyMarkdownString(item.hover.contents)) .map(item => new MarkdownHover(this, part.item.anchor.range, item.hover.contents, false, 2 + item.ordinal)); } finally { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts index 21abd0b7093..47ee2e9f972 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts @@ -72,7 +72,7 @@ export class GhostTextPart { ) { } - readonly lines = splitLines(this.text);; + readonly lines = splitLines(this.text); equals(other: GhostTextPart): boolean { return this.column === other.column && diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts index 0c93d8465ab..450f9549fb7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts @@ -34,7 +34,7 @@ export interface IGhostTextWidgetModel { export class GhostTextWidget extends Disposable { private readonly isDisposed = observableValue(this, false); - private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); + private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); constructor( private readonly editor: ICodeEditor, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts index 96b27ee7bf5..7bcf0ea59f1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts @@ -12,7 +12,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IModelDecoration } from 'vs/editor/common/model'; -import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -94,8 +94,8 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan return []; } - renderHoverParts(context: IEditorHoverRenderContext, hoverParts: InlineCompletionsHover[]): IDisposable { - const disposableStore = new DisposableStore(); + renderHoverParts(context: IEditorHoverRenderContext, hoverParts: InlineCompletionsHover[]): IRenderedHoverParts { + const disposables = new DisposableStore(); const part = hoverParts[0]; this._telemetryService.publicLog2<{}, { @@ -104,7 +104,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan }>('inlineCompletionHover.shown'); if (this.accessibilityService.isScreenReaderOptimized() && !this._editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - this.renderScreenReaderText(context, part, disposableStore); + disposables.add(this.renderScreenReaderText(context, part)); } const model = part.controller.model.get()!; @@ -113,34 +113,44 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan constObservable(null), model.selectedInlineCompletionIndex, model.inlineCompletionsCount, - model.selectedInlineCompletion.map(v => /** @description commands */ v?.inlineCompletion.source.inlineCompletions.commands ?? []), + model.activeCommands, ); - context.fragment.appendChild(w.getDomNode()); + const widgetNode: HTMLElement = w.getDomNode(); + context.fragment.appendChild(widgetNode); model.triggerExplicitly(); - disposableStore.add(w); - - return disposableStore; + disposables.add(w); + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: part, + hoverElement: widgetNode, + dispose() { disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); } - private renderScreenReaderText(context: IEditorHoverRenderContext, part: InlineCompletionsHover, disposableStore: DisposableStore) { + getAccessibleContent(hoverPart: InlineCompletionsHover): string { + return nls.localize('hoverAccessibilityStatusBar', 'There are inline completions here'); + } + + private renderScreenReaderText(context: IEditorHoverRenderContext, part: InlineCompletionsHover): IDisposable { + const disposables = new DisposableStore(); const $ = dom.$; const markdownHoverElement = $('div.hover-row.markdown-hover'); const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents', { ['aria-live']: 'assertive' })); - const renderer = disposableStore.add(new MarkdownRenderer({ editor: this._editor }, this._languageService, this._openerService)); + const renderer = disposables.add(new MarkdownRenderer({ editor: this._editor }, this._languageService, this._openerService)); const render = (code: string) => { - disposableStore.add(renderer.onDidRenderAsync(() => { + disposables.add(renderer.onDidRenderAsync(() => { hoverContentsElement.className = 'hover-contents code-hover-contents'; context.onContentsChanged(); })); const inlineSuggestionAvailable = nls.localize('inlineSuggestionFollows', "Suggestion:"); - const renderedContents = disposableStore.add(renderer.render(new MarkdownString().appendText(inlineSuggestionAvailable).appendCodeblock('text', code))); + const renderedContents = disposables.add(renderer.render(new MarkdownString().appendText(inlineSuggestionAvailable).appendCodeblock('text', code))); hoverContentsElement.replaceChildren(renderedContents.element); }; - disposableStore.add(autorun(reader => { + disposables.add(autorun(reader => { /** @description update hover */ const ghostText = part.controller.model.read(reader)?.primaryGhostText.read(reader); if (ghostText) { @@ -152,5 +162,6 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan })); context.fragment.appendChild(markdownHoverElement); + return disposables; } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts index ff80048cc4d..2819af72061 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts @@ -7,7 +7,9 @@ import { EditorContributionInstantiation, registerEditorAction, registerEditorCo import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { TriggerInlineSuggestionAction, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, AcceptNextWordOfInlineCompletion, AcceptInlineCompletion, HideInlineCompletion, ToggleAlwaysShowInlineSuggestionToolbar, AcceptNextLineOfInlineCompletion } from 'vs/editor/contrib/inlineCompletions/browser/commands'; import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/browser/hoverParticipant'; +import { InlineCompletionsAccessibleView } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { registerAction2 } from 'vs/platform/actions/common/actions'; registerEditorContribution(InlineCompletionsController.ID, InlineCompletionsController, EditorContributionInstantiation.Eventually); @@ -22,3 +24,5 @@ registerEditorAction(HideInlineCompletion); registerAction2(ToggleAlwaysShowInlineSuggestionToolbar); HoverParticipantRegistry.register(InlineCompletionsHoverParticipant); + +AccessibleViewRegistry.register(new InlineCompletionsAccessibleView()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts new file mode 100644 index 00000000000..a028db8d081 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; +import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { AccessibleViewType, AccessibleViewProviderId, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; + +export class InlineCompletionsAccessibleView implements IAccessibleViewImplentation { + readonly type = AccessibleViewType.View; + readonly priority = 95; + readonly name = 'inline-completions'; + readonly when = ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!editor) { + return; + } + + const model = InlineCompletionsController.get(editor)?.model.get(); + if (!model?.state.get()) { + return; + } + + return new InlineCompletionsAccessibleViewContentProvider(editor, model); + } +} + +class InlineCompletionsAccessibleViewContentProvider extends Disposable implements IAccessibleViewContentProvider { + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + constructor( + private readonly _editor: ICodeEditor, + private readonly _model: InlineCompletionsModel, + ) { + super(); + } + + public readonly id = AccessibleViewProviderId.InlineCompletions; + public readonly verbositySettingKey = 'accessibility.verbosity.inlineCompletions'; + public readonly options = { language: this._editor.getModel()?.getLanguageId() ?? undefined, type: AccessibleViewType.View }; + + public provideContent(): string { + 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'); + } + return lineText + ghostText; + } + public provideNextContent(): string | undefined { + // asynchronously update the model and fire the event + this._model.next().then((() => this._onDidChangeContent.fire())); + return; + } + public providePreviousContent(): string | undefined { + // asynchronously update the model and fire the event + this._model.previous().then((() => this._onDidChangeContent.fire())); + return; + } + public onClose(): void { + this._model.stop(); + this._editor.focus(); + } +} + diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts index 320a8f17264..a7c25e42677 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts @@ -3,34 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createStyleSheet2 } from 'vs/base/browser/dom'; +import { createStyleSheetFromObservable } from 'vs/base/browser/domObservable'; import { alert } from 'vs/base/browser/ui/aria/aria'; +import { timeout } from 'vs/base/common/async'; +import { cancelOnDispose } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, ITransaction, autorun, autorunHandleChanges, constObservable, derived, disposableObservableValue, observableFromEvent, observableSignal, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, ITransaction, autorun, constObservable, derived, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from 'vs/base/common/observable'; +import { ISettableObservable } from 'vs/base/common/observableInternal/base'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { derivedObservableWithCache, mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; +import { isUndefined } from 'vs/base/common/types'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor, reactToChange, reactToChangeWithStore } from 'vs/editor/browser/observableCodeEditor'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds'; import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; -import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; import { localize } from 'vs/nls'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; -import { ISettableObservable } from 'vs/base/common/observableInternal/base'; export class InlineCompletionsController extends Disposable { static ID = 'editor.contrib.inlineCompletionsController'; @@ -39,38 +44,33 @@ export class InlineCompletionsController extends Disposable { return editor.getContribution(InlineCompletionsController.ID); } - public readonly model = this._register(disposableObservableValue('inlineCompletionModel', undefined)); - private readonly _textModelVersionId = observableValue(this, -1); - private readonly _positions = observableValue(this, [new Position(1, 1)]); + private readonly _editorObs = observableCodeEditor(this.editor); + private readonly _positions = derived(this, reader => this._editorObs.selections.read(reader)?.map(s => s.getEndPosition()) ?? [new Position(1, 1)]); + private readonly _suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor( this.editor, - () => this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined), - (tx) => this.updateObservables(tx, VersionIdChangeReason.Other), - (item) => { - transaction(tx => { - /** @description InlineCompletionsController.handleSuggestAccepted */ - this.updateObservables(tx, VersionIdChangeReason.Other); - this.model.get()?.handleSuggestAccepted(item); - }); - } + () => { + this._editorObs.forceUpdate(); + return this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined); + }, + (item) => this._editorObs.forceUpdate(_tx => { + /** @description InlineCompletionsController.handleSuggestAccepted */ + this.model.get()?.handleSuggestAccepted(item); + }) )); - private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); - private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily); - private readonly _ghostTexts = derived(this, (reader) => { - const model = this.model.read(reader); - return model?.ghostTexts.read(reader) ?? []; - }); + private readonly _suggestWidgetSelectedItem = observableFromEvent(this, cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => { + this._editorObs.forceUpdate(_tx => cb(undefined)); + }), () => this._suggestWidgetAdaptor.selectedItem); - private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); - private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => { - return store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, { - ghostText: ghostText, - minReservedLineCount: constObservable(0), - targetTextModel: this.model.map(v => v?.textModel), - })); - }).recomputeInitiallyAndOnChange(this._store); + private readonly _enabledInConfig = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); + private readonly _isScreenReaderEnabled = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + private readonly _editorDictationInProgress = observableFromEvent(this, + this._contextKeyService.onDidChangeContext, + () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true + ); + private readonly _enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); private readonly _debounceValue = this._debounceService.for( this._languageFeaturesService.inlineCompletionsProvider, @@ -78,11 +78,43 @@ export class InlineCompletionsController extends Disposable { { min: 50, max: 50 } ); + public readonly model = derivedDisposable(this, reader => { + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineCompletionsModel = this._instantiationService.createInstance( + InlineCompletionsModel, + textModel, + this._suggestWidgetSelectedItem, + this._editorObs.versionId, + this._positions, + this._debounceValue, + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.suggest).preview), + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.suggest).previewMode), + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).mode), + this._enabled, + ); + return model; + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _ghostTexts = derived(this, (reader) => { + const model = this.model.read(reader); + return model?.ghostTexts.read(reader) ?? []; + }); + private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); + + private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => + store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, { + ghostText: ghostText, + minReservedLineCount: constObservable(0), + targetTextModel: this.model.map(v => v?.textModel), + })) + ).recomputeInitiallyAndOnChange(this._store); + private readonly _playAccessibilitySignal = observableSignal(this); - private readonly _isReadonly = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); - private readonly _textModel = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel()); - private readonly _textModelIfWritable = derived(reader => this._isReadonly.read(reader) ? undefined : this._textModel.read(reader)); + private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily); constructor( public readonly editor: ICodeEditor, @@ -94,74 +126,17 @@ export class InlineCompletionsController extends Disposable { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { super(); this._register(new InlineCompletionContextKeys(this._contextKeyService, this.model)); - this._register(autorun(reader => { - /** @description InlineCompletionsController.update model */ - const textModel = this._textModelIfWritable.read(reader); - transaction(tx => { - /** @description InlineCompletionsController.onDidChangeModel/readonly */ - this.model.set(undefined, tx); - this.updateObservables(tx, VersionIdChangeReason.Other); - - if (textModel) { - const model = _instantiationService.createInstance( - InlineCompletionsModel, - textModel, - this._suggestWidgetAdaptor.selectedItem, - this._textModelVersionId, - this._positions, - this._debounceValue, - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).preview), - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).previewMode), - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.inlineSuggest).mode), - this._enabled, - ); - this.model.set(model, tx); - } - }); - })); - - const styleElement = this._register(createStyleSheet2()); - this._register(autorun(reader => { - const fontFamily = this._fontFamily.read(reader); - styleElement.setStyle(fontFamily === '' || fontFamily === 'default' ? `` : ` -.monaco-editor .ghost-text-decoration, -.monaco-editor .ghost-text-decoration-preview, -.monaco-editor .ghost-text { - font-family: ${fontFamily}; -}`); - })); - - const getReason = (e: IModelContentChangedEvent): VersionIdChangeReason => { - if (e.isUndoing) { return VersionIdChangeReason.Undo; } - if (e.isRedoing) { return VersionIdChangeReason.Redo; } - if (this.model.get()?.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; } - return VersionIdChangeReason.Other; - }; - this._register(editor.onDidChangeModelContent((e) => transaction(tx => - /** @description InlineCompletionsController.onDidChangeModelContent */ - this.updateObservables(tx, getReason(e)) - ))); - - this._register(editor.onDidChangeCursorPosition(e => transaction(tx => { - /** @description InlineCompletionsController.onDidChangeCursorPosition */ - this.updateObservables(tx, VersionIdChangeReason.Other); - if (e.reason === CursorChangeReason.Explicit || e.source === 'api') { - this.model.get()?.stop(tx); - } - }))); - - this._register(editor.onDidType(() => transaction(tx => { - /** @description InlineCompletionsController.onDidType */ - this.updateObservables(tx, VersionIdChangeReason.Other); + this._register(reactToChange(this._editorObs.onDidType, (_value, _changes) => { if (this._enabled.get()) { - this.model.get()?.trigger(tx); + this.model.get()?.trigger(); } - }))); + })); this._register(this._commandService.onDidExecuteCommand((e) => { // These commands don't trigger onDidType. @@ -173,22 +148,28 @@ export class InlineCompletionsController extends Disposable { 'acceptSelectedSuggestion', ]); if (commands.has(e.commandId) && editor.hasTextFocus() && this._enabled.get()) { - transaction(tx => { + this._editorObs.forceUpdate(tx => { /** @description onDidExecuteCommand */ this.model.get()?.trigger(tx); }); } })); + this._register(reactToChange(this._editorObs.selections, (_value, changes) => { + if (changes.some(e => e.reason === CursorChangeReason.Explicit || e.source === 'api')) { + this.model.get()?.stop(); + } + })); + this._register(this.editor.onDidBlurEditorWidget(() => { // This is a hidden setting very useful for debugging - if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') || - editor.getOption(EditorOption.inlineSuggest).keepOnBlur) { - return; - } - if (InlineSuggestionHintsContentWidget.dropDownVisible) { + if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') + || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') + || editor.getOption(EditorOption.inlineSuggest).keepOnBlur + || InlineSuggestionHintsContentWidget.dropDownVisible) { return; } + transaction(tx => { /** @description InlineCompletionsController.onDidBlurEditorWidget */ this.model.get()?.stop(tx); @@ -210,37 +191,48 @@ export class InlineCompletionsController extends Disposable { this._suggestWidgetAdaptor.stopForceRenderingAbove(); })); - let lastInlineCompletionId: string | undefined = undefined; - this._register(autorunHandleChanges({ - handleChange: (context, changeSummary) => { - if (context.didChange(this._playAccessibilitySignal)) { - lastInlineCompletionId = undefined; - } - return true; - }, - }, async reader => { - /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ - this._playAccessibilitySignal.read(reader); - + const currentInlineCompletionBySemanticId = derivedObservableWithCache(this, (reader, last) => { const model = this.model.read(reader); const state = model?.state.read(reader); - if (!model || !state || !state.inlineCompletion) { - lastInlineCompletionId = undefined; - return; + if (this._suggestWidgetSelectedItem.get()) { + return last; } + return state?.inlineCompletion?.semanticId; + }); + this._register(reactToChangeWithStore(derived(reader => { + this._playAccessibilitySignal.read(reader); + currentInlineCompletionBySemanticId.read(reader); + return {}; + }), async (_value, _deltas, store) => { + /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ + const model = this.model.get(); + const state = model?.state.get(); + if (!state || !model) { return; } + const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - if (state.inlineCompletion.semanticId !== lastInlineCompletionId) { - lastInlineCompletionId = state.inlineCompletion.semanticId; - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion).then(() => { - if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - this.provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); - } - }); + await timeout(50, cancelOnDispose(store)); + await waitForState(this._suggestWidgetSelectedItem, isUndefined, () => false, cancelOnDispose(store)); + + await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); + if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { + this._provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); } })); this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this._instantiationService)); + + this._register(createStyleSheetFromObservable(derived(reader => { + const fontFamily = this._fontFamily.read(reader); + if (fontFamily === '' || fontFamily === 'default') { return ''; } + return ` +.monaco-editor .ghost-text-decoration, +.monaco-editor .ghost-text-decoration-preview, +.monaco-editor .ghost-text { + font-family: ${fontFamily}; +}`; + }))); + + // TODO@hediet this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('accessibility.verbosity.inlineCompletions')) { this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.inlineCompletions') }); @@ -253,25 +245,14 @@ export class InlineCompletionsController extends Disposable { this._playAccessibilitySignal.trigger(tx); } - private provideScreenReaderUpdate(content: string): void { + private _provideScreenReaderUpdate(content: string): void { const accessibleViewShowing = this._contextKeyService.getContextKeyValue('accessibleViewIsShown'); const accessibleViewKeybinding = this._keybindingService.lookupKeybinding('editor.action.accessibleView'); let hint: string | undefined; if (!accessibleViewShowing && accessibleViewKeybinding && this.editor.getOption(EditorOption.inlineCompletionsAccessibilityVerbose)) { hint = localize('showAccessibleViewHint', "Inspect this in the accessible view ({0})", accessibleViewKeybinding.getAriaLabel()); } - hint ? alert(content + ', ' + hint) : alert(content); - } - - /** - * Copies over the relevant state from the text model to observables. - * This solves all kind of eventing issues, as we make sure we always operate on the latest state, - * regardless of who calls into us. - */ - private updateObservables(tx: ITransaction, changeReason: VersionIdChangeReason): void { - const newModel = this.editor.getModel(); - this._textModelVersionId.set(newModel?.getVersionId() ?? -1, tx, changeReason); - this._positions.set(this.editor.getSelections()?.map(selection => selection.getPosition()) ?? [new Position(1, 1)], tx); + alert(hint ? content + ', ' + hint : content); } public shouldShowHoverAt(range: Range) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts index e847839c042..2226a1ea265 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts @@ -11,7 +11,8 @@ import { equals } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, autorunWithStore, derived, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from 'vs/base/common/observable'; +import { derivedWithStore } from 'vs/base/common/observableInternal/derived'; import { OS } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./inlineCompletionsHintsWidget'; @@ -35,7 +36,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export class InlineCompletionsHintsWidget extends Disposable { - private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); + private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); private sessionPosition: Position | undefined = undefined; @@ -71,26 +72,36 @@ export class InlineCompletionsHintsWidget extends Disposable { return; } - const contentWidget = store.add(this.instantiationService.createInstance( - InlineSuggestionHintsContentWidget, - this.editor, - true, - this.position, - model.selectedInlineCompletionIndex, - model.inlineCompletionsCount, - model.selectedInlineCompletion.map(v => /** @description commands */ v?.inlineCompletion.source.inlineCompletions.commands ?? []), - )); - editor.addContentWidget(contentWidget); - store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); + const contentWidgetValue = derivedWithStore((reader, store) => { + const contentWidget = store.add(this.instantiationService.createInstance( + InlineSuggestionHintsContentWidget, + this.editor, + true, + this.position, + model.selectedInlineCompletionIndex, + model.inlineCompletionsCount, + model.activeCommands, + )); + editor.addContentWidget(contentWidget); + store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); + store.add(autorun(reader => { + /** @description request explicit */ + const position = this.position.read(reader); + if (!position) { + return; + } + if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) { + model.triggerExplicitly(); + } + })); + return contentWidget; + }); + + const hadPosition = derivedObservableWithCache(this, (reader, lastValue) => !!this.position.read(reader) || !!lastValue); store.add(autorun(reader => { - /** @description request explicit */ - const position = this.position.read(reader); - if (!position) { - return; - } - if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) { - model.triggerExplicitly(); + if (hadPosition.read(reader)) { + contentWidgetValue.read(reader); } })); })); @@ -151,8 +162,6 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC this.previousAction.enabled = this.nextAction.enabled = false; }, 100)); - private lastCommands: Command[] = []; - constructor( private readonly editor: ICodeEditor, private readonly withBorder: boolean, @@ -225,13 +234,6 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC this._register(autorun(reader => { /** @description extra commands */ const extraCommands = this._extraCommands.read(reader); - if (equals(this.lastCommands, extraCommands)) { - // nothing to update - return; - } - - this.lastCommands = extraCommands; - const extraActions = extraCommands.map(c => ({ class: undefined, id: c.id, @@ -328,9 +330,10 @@ export class CustomizedMenuWorkbenchToolBar extends WorkbenchToolBar { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, + @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, telemetryService); + super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); this._store.add(this.menu.onDidChange(() => this.updateToolbar())); this.updateToolbar(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 996471bde48..92013fd5982 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Permutation } from 'vs/base/common/arrays'; +import { compareBy, Permutation } from 'vs/base/common/arrays'; import { mapFindFirst } from 'vs/base/common/arraysFind'; +import { itemsEquals } from 'vs/base/common/equals'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, recomputeInitiallyAndOnChange, observableSignal, observableValue, subtransaction, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from 'vs/base/common/observable'; import { commonPrefixLength, splitLinesIncludeSeparators } from 'vs/base/common/strings'; import { isDefined } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -15,32 +16,27 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from 'vs/editor/common/languages'; +import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { TextLength } from 'vs/editor/common/core/textLength'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { Command, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from 'vs/editor/common/languages'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource'; -import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { computeGhostText, singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; import { SuggestItemInfo } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; import { addPositions, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { singleTextEditAugments, computeGhostText, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; -import { TextLength } from 'vs/editor/common/core/textLength'; - -export enum VersionIdChangeReason { - Undo, - Redo, - AcceptWord, - Other, -} export class InlineCompletionsModel extends Disposable { - private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this.textModelVersionId, this._debounceValue)); - private readonly _isActive = observableValue(this, false); - readonly _forceUpdateSignal = observableSignal('forceUpdate'); + private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue)); + private readonly _isActive = observableValue(this, false); + private readonly _forceUpdateExplicitlySignal = observableSignal(this); // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. private readonly _selectedInlineCompletionId = observableValue(this, undefined); @@ -52,7 +48,7 @@ export class InlineCompletionsModel extends Disposable { constructor( public readonly textModel: ITextModel, public readonly selectedSuggestItem: IObservable, - public readonly textModelVersionId: IObservable, + public readonly _textModelVersionId: IObservable, private readonly _positions: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, private readonly _suggestPreviewEnabled: IObservable, @@ -65,7 +61,7 @@ export class InlineCompletionsModel extends Disposable { ) { super(); - this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletions)); + this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise)); let lastItem: InlineCompletionWithUpdatedRange | undefined = undefined; this._register(autorun(reader => { @@ -88,7 +84,15 @@ export class InlineCompletionsModel extends Disposable { VersionIdChangeReason.Undo, VersionIdChangeReason.AcceptWord, ]); - private readonly _fetchInlineCompletions = derivedHandleChanges({ + + private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason { + if (e?.isUndoing) { return VersionIdChangeReason.Undo; } + if (e?.isRedoing) { return VersionIdChangeReason.Redo; } + if (this.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; } + return VersionIdChangeReason.Other; + } + + private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({ owner: this, createEmptyChangeSummary: () => ({ preserveCurrentCompletion: false, @@ -96,26 +100,22 @@ export class InlineCompletionsModel extends Disposable { }), handleChange: (ctx, changeSummary) => { /** @description fetch inline completions */ - if (ctx.didChange(this.textModelVersionId) && this._preserveCurrentCompletionReasons.has(ctx.change)) { + if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { changeSummary.preserveCurrentCompletion = true; - } else if (ctx.didChange(this._forceUpdateSignal)) { - changeSummary.inlineCompletionTriggerKind = ctx.change; + } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; } return true; }, }, (reader, changeSummary) => { - this._forceUpdateSignal.read(reader); + this._forceUpdateExplicitlySignal.read(reader); const shouldUpdate = (this._enabled.read(reader) && this.selectedSuggestItem.read(reader)) || this._isActive.read(reader); if (!shouldUpdate) { this._source.cancelUpdate(); return undefined; } - this.textModelVersionId.read(reader); // Refetch on text change - - const itemToPreserveCandidate = this.selectedInlineCompletion.get(); - const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable - ? itemToPreserveCandidate : undefined; + this._textModelVersionId.read(reader); // Refetch on text change const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); const suggestItem = this.selectedSuggestItem.read(reader); @@ -135,20 +135,23 @@ export class InlineCompletionsModel extends Disposable { triggerKind: changeSummary.inlineCompletionTriggerKind, selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(), }; + const itemToPreserveCandidate = this.selectedInlineCompletion.get(); + const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable + ? itemToPreserveCandidate : undefined; return this._source.fetch(cursorPosition, context, itemToPreserve); }); public async trigger(tx?: ITransaction): Promise { this._isActive.set(true, tx); - await this._fetchInlineCompletions.get(); + await this._fetchInlineCompletionsPromise.get(); } public async triggerExplicitly(tx?: ITransaction): Promise { subtransaction(tx, tx => { this._isActive.set(true, tx); - this._forceUpdateSignal.trigger(tx, InlineCompletionTriggerKind.Explicit); + this._forceUpdateExplicitlySignal.trigger(tx); }); - await this._fetchInlineCompletions.get(); + await this._fetchInlineCompletionsPromise.get(); } public stop(tx?: ITransaction): void { @@ -158,7 +161,7 @@ export class InlineCompletionsModel extends Disposable { }); } - private readonly _filteredInlineCompletionItems = derived(this, reader => { + private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { const c = this._source.inlineCompletions.read(reader); if (!c) { return []; } const cursorPosition = this._primaryPosition.read(reader); @@ -185,6 +188,10 @@ export class InlineCompletionsModel extends Disposable { return filteredCompletions[idx]; }); + public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, + r => this.selectedInlineCompletion.read(r)?.inlineCompletion.source.inlineCompletions.commands ?? [] + ); + public readonly lastTriggerKind: IObservable = this._source.inlineCompletions.map(this, v => v?.request.context.triggerKind); @@ -204,7 +211,7 @@ export class InlineCompletionsModel extends Disposable { inlineCompletion: InlineCompletionWithUpdatedRange | undefined; } | undefined>({ owner: this, - equalityComparer: (a, b) => { + equalsFn: (a, b) => { if (!a || !b) { return a === b; } return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) && a.inlineCompletion === b.inlineCompletion @@ -258,26 +265,24 @@ export class InlineCompletionsModel extends Disposable { const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => { let r = completion.toSingleTextEdit(reader); - r = singleTextRemoveCommonPrefix(r, model, Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())); + r = singleTextRemoveCommonPrefix( + r, + model, + Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition()) + ); return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined; }); return augmentedCompletion; } - public readonly ghostTexts = derivedOpts({ - owner: this, - equalityComparer: ghostTextsOrReplacementsEqual - }, reader => { + public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { const v = this.state.read(reader); if (!v) { return undefined; } return v.ghostTexts; }); - public readonly primaryGhostText = derivedOpts({ - owner: this, - equalityComparer: ghostTextOrReplacementEquals - }, reader => { + public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => { const v = this.state.read(reader); if (!v) { return undefined; } return v?.primaryGhostText; @@ -314,12 +319,17 @@ export class InlineCompletionsModel extends Disposable { } const completion = state.inlineCompletion.toInlineCompletion(undefined); + if (completion.command) { + // Make sure the completion list will not be disposed. + completion.source.addRef(); + } + editor.pushUndoStop(); if (completion.snippetInfo) { editor.executeEdits( 'inlineSuggestion.accept', [ - EditOperation.replaceMove(completion.range, ''), + EditOperation.replace(completion.range, ''), ...completion.additionalTextEdits ] ); @@ -329,24 +339,14 @@ export class InlineCompletionsModel extends Disposable { const edits = state.edits; const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); editor.executeEdits('inlineSuggestion.accept', [ - ...edits.map(edit => EditOperation.replaceMove(edit.range, edit.text)), + ...edits.map(edit => EditOperation.replace(edit.range, edit.text)), ...completion.additionalTextEdits ]); editor.setSelections(selections, 'inlineCompletionAccept'); } - if (completion.command) { - // Make sure the completion list will not be disposed. - completion.source.addRef(); - } - - // Reset before invoking the command, since the command might cause a follow up trigger. - transaction(tx => { - this._source.clear(tx); - // Potentially, isActive will get set back to true by the typing or accept inline suggest event - // if automatic inline suggestions are enabled. - this._isActive.set(false, tx); - }); + // Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset). + this.stop(); if (completion.command) { await this._commandService @@ -437,8 +437,9 @@ export class InlineCompletionsModel extends Disposable { const primaryEdit = new SingleTextEdit(replaceRange, newText); const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)]; const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); - editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replaceMove(edit.range, edit.text))); + editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replace(edit.range, edit.text))); editor.setSelections(selections, 'inlineCompletionPartialAccept'); + editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Immediate); } finally { this._isAcceptingPartially = false; } @@ -451,9 +452,7 @@ export class InlineCompletionsModel extends Disposable { completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, - { - kind, - } + { kind, } ); } } finally { @@ -478,6 +477,13 @@ export class InlineCompletionsModel extends Disposable { } } +export enum VersionIdChangeReason { + Undo, + Redo, + AcceptWord, + Other, +} + export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { if (positions.length === 1) { // No secondary cursor positions @@ -520,7 +526,7 @@ function substringPos(text: string, pos: Position): string { } function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { - const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range)); + const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); const edit = new TextEdit(sortPerm.apply(edits)); const sortedNewRanges = edit.getNewRanges(); const newRanges = sortPerm.inverse().apply(sortedNewRanges); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts index 94a8b33d477..1eb06aa11c1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts @@ -4,18 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { equalsIfDefined, itemEquals } from 'vs/base/common/equals'; import { matchesSubString } from 'vs/base/common/filters'; import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ITransaction, derived, disposableObservableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, ITransaction, derivedOpts, disposableObservableValue, transaction } from 'vs/base/common/observable'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; +import { TextLength } from 'vs/editor/common/core/textLength'; import { InlineCompletionContext, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions'; -import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class InlineCompletionsSource extends Disposable { @@ -25,7 +27,7 @@ export class InlineCompletionsSource extends Disposable { constructor( private readonly textModel: ITextModel, - private readonly versionId: IObservable, + private readonly versionId: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, @@ -60,7 +62,7 @@ export class InlineCompletionsSource extends Disposable { await wait(this._debounceValue.get(this.textModel), source.token); } - if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { return false; } @@ -74,7 +76,7 @@ export class InlineCompletionsSource extends Disposable { this.languageConfigurationService ); - if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { return false; } @@ -149,20 +151,13 @@ class UpdateRequest { public satisfies(other: UpdateRequest): boolean { return this.position.equals(other.position) - && equals(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, (v1, v2) => v1.equals(v2)) + && equalsIfDefined(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, itemEquals()) && (other.context.triggerKind === InlineCompletionTriggerKind.Automatic || this.context.triggerKind === InlineCompletionTriggerKind.Explicit) && this.versionId === other.versionId; } } -function equals(v1: T | undefined, v2: T | undefined, equals: (v1: T, v2: T) => boolean): boolean { - if (!v1 || !v2) { - return v1 === v2; - } - return equals(v1, v2); -} - class UpdateOperation implements IDisposable { constructor( public readonly request: UpdateRequest, @@ -183,26 +178,13 @@ export class UpToDateInlineCompletions implements IDisposable { private _refCount = 1; private readonly _prependedInlineCompletionItems: InlineCompletionItem[] = []; - private _rangeVersionIdValue = 0; - private readonly _rangeVersionId = derived(this, reader => { - this.versionId.read(reader); - let changed = false; - for (const i of this._inlineCompletions) { - changed = changed || i._updateRange(this.textModel); - } - if (changed) { - this._rangeVersionIdValue++; - } - return this._rangeVersionIdValue; - }); - constructor( private readonly inlineCompletionProviderResult: InlineCompletionProviderResult, public readonly request: UpdateRequest, - private readonly textModel: ITextModel, - private readonly versionId: IObservable, + private readonly _textModel: ITextModel, + private readonly _versionId: IObservable, ) { - const ids = textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({ + const ids = _textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({ range: i.range, options: { description: 'inline-completion-tracking-range' @@ -210,7 +192,7 @@ export class UpToDateInlineCompletions implements IDisposable { }))); this._inlineCompletions = inlineCompletionProviderResult.completions.map( - (i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._rangeVersionId) + (i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._textModel, this._versionId) ); } @@ -224,9 +206,9 @@ export class UpToDateInlineCompletions implements IDisposable { if (this._refCount === 0) { setTimeout(() => { // To fix https://github.com/microsoft/vscode/issues/188348 - if (!this.textModel.isDisposed()) { + if (!this._textModel.isDisposed()) { // This is just cleanup. It's ok if it happens with a delay. - this.textModel.deltaDecorations(this._inlineCompletions.map(i => i.decorationId), []); + this._textModel.deltaDecorations(this._inlineCompletions.map(i => i.decorationId), []); } }, 0); this.inlineCompletionProviderResult.dispose(); @@ -241,13 +223,13 @@ export class UpToDateInlineCompletions implements IDisposable { inlineCompletion.source.addRef(); } - const id = this.textModel.deltaDecorations([], [{ + const id = this._textModel.deltaDecorations([], [{ range, options: { description: 'inline-completion-tracking-range' }, }])[0]; - this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._rangeVersionId, range)); + this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._textModel, this._versionId)); this._prependedInlineCompletionItems.push(inlineCompletion); } } @@ -258,36 +240,38 @@ export class InlineCompletionWithUpdatedRange { this.inlineCompletion.insertText, this.inlineCompletion.range.getStartPosition().toString() ]); - private _updatedRange: Range; - private _isValid = true; public get forwardStable() { return this.inlineCompletion.source.inlineCompletions.enableForwardStability ?? false; } + private readonly _updatedRange = derivedOpts({ owner: this, equalsFn: Range.equalsRange }, reader => { + this._modelVersion.read(reader); + return this._textModel.getDecorationRange(this.decorationId); + }); + constructor( public readonly inlineCompletion: InlineCompletionItem, public readonly decorationId: string, - private readonly rangeVersion: IObservable, - initialRange?: Range, + private readonly _textModel: ITextModel, + private readonly _modelVersion: IObservable, ) { - this._updatedRange = initialRange ?? inlineCompletion.range; } public toInlineCompletion(reader: IReader | undefined): InlineCompletionItem { - return this.inlineCompletion.withRange(this._getUpdatedRange(reader)); + return this.inlineCompletion.withRange(this._updatedRange.read(reader) ?? emptyRange); } public toSingleTextEdit(reader: IReader | undefined): SingleTextEdit { - return new SingleTextEdit(this._getUpdatedRange(reader), this.inlineCompletion.insertText); + return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.insertText); } public isVisible(model: ITextModel, cursorPosition: Position, reader: IReader | undefined): boolean { const minimizedReplacement = singleTextRemoveCommonPrefix(this._toFilterTextReplacement(reader), model); - + const updatedRange = this._updatedRange.read(reader); if ( - !this._isValid - || !this.inlineCompletion.range.getStartPosition().equals(this._getUpdatedRange(reader).getStartPosition()) + !updatedRange + || !this.inlineCompletion.range.getStartPosition().equals(updatedRange.getStartPosition()) || cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber ) { return false; @@ -323,45 +307,17 @@ export class InlineCompletionWithUpdatedRange { } public canBeReused(model: ITextModel, position: Position): boolean { - const result = this._isValid - && this._getUpdatedRange(undefined).containsPosition(position) + const updatedRange = this._updatedRange.read(undefined); + const result = !!updatedRange + && updatedRange.containsPosition(position) && this.isVisible(model, position, undefined) - && !this._isSmallerThanOriginal(undefined); + && TextLength.ofRange(updatedRange).isGreaterThanOrEqualTo(TextLength.ofRange(this.inlineCompletion.range)); return result; } private _toFilterTextReplacement(reader: IReader | undefined): SingleTextEdit { - return new SingleTextEdit(this._getUpdatedRange(reader), this.inlineCompletion.filterText); - } - - private _isSmallerThanOriginal(reader: IReader | undefined): boolean { - return length(this._getUpdatedRange(reader)).isBefore(length(this.inlineCompletion.range)); - } - - private _getUpdatedRange(reader: IReader | undefined): Range { - this.rangeVersion.read(reader); // This makes sure all the ranges are updated. - return this._updatedRange; - } - - public _updateRange(textModel: ITextModel): boolean { - const range = textModel.getDecorationRange(this.decorationId); - if (!range) { - // A setValue call might flush all decorations. - this._isValid = false; - return true; - } - if (!this._updatedRange.equalsRange(range)) { - this._updatedRange = range; - return true; - } - return false; + return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.filterText); } } -function length(range: Range): Position { - if (range.startLineNumber === range.endLineNumber) { - return new Position(1, 1 + range.endColumn - range.startColumn); - } else { - return new Position(1 + range.endLineNumber - range.startLineNumber, range.endColumn); - } -} +const emptyRange = new Range(1, 1, 1, 1); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts index 28052040c32..25ccf4cd125 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts @@ -18,19 +18,19 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { ITextModel } from 'vs/editor/common/model'; import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; -import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { getReadonlyEmptyArray } from './utils'; import { SnippetParser, Text } from 'vs/editor/contrib/snippet/browser/snippetParser'; export async function provideInlineCompletions( registry: LanguageFeatureRegistry, - position: Position, + positionOrRange: Position | Range, model: ITextModel, context: InlineCompletionContext, token: CancellationToken = CancellationToken.None, languageConfigurationService?: ILanguageConfigurationService, ): Promise { // Important: Don't use position after the await calls, as the model could have been changed in the meantime! - const defaultReplaceRange = getDefaultRange(position, model); + const defaultReplaceRange = positionOrRange instanceof Position ? getDefaultRange(positionOrRange, model) : positionOrRange; const providers = registry.all(model); const multiMap = new SetMap>(); @@ -100,8 +100,13 @@ export async function provideInlineCompletions( } try { - const completions = await provider.provideInlineCompletions(model, position, context, token); - return completions; + if (positionOrRange instanceof Position) { + const completions = await provider.provideInlineCompletions(model, positionOrRange, context, token); + return completions; + } else { + const completions = await provider.provideInlineEdits?.(model, positionOrRange, context, token); + return completions; + } } catch (e) { onUnexpectedExternalError(e); return undefined; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts index 5f98b45033a..daf73173ef4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts @@ -3,39 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; +import { compareBy, numberComparator } from 'vs/base/common/arrays'; +import { findFirstMax } from 'vs/base/common/arraysFind'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession'; import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { IObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; -import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; -import { ITextModel } from 'vs/editor/common/model'; -import { compareBy, numberComparator } from 'vs/base/common/arrays'; -import { findFirstMaxBy } from 'vs/base/common/arraysFind'; -import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class SuggestWidgetAdaptor extends Disposable { private isSuggestWidgetVisible: boolean = false; private isShiftKeyPressed = false; private _isActive = false; private _currentSuggestItemInfo: SuggestItemInfo | undefined = undefined; - - private readonly _selectedItem = observableValue(this, undefined as SuggestItemInfo | undefined); - - public get selectedItem(): IObservable { - return this._selectedItem; + public get selectedItem(): SuggestItemInfo | undefined { + return this._currentSuggestItemInfo; } + private _onDidSelectedItemChange = this._register(new Emitter()); + public readonly onDidSelectedItemChange: Event = this._onDidSelectedItemChange.event; constructor( private readonly editor: ICodeEditor, private readonly suggestControllerPreselector: () => SingleTextEdit | undefined, - private readonly checkModelVersion: (tx: ITransaction) => void, private readonly onWillAccept: (item: SuggestItemInfo) => void, ) { super(); @@ -59,8 +56,6 @@ export class SuggestWidgetAdaptor extends Disposable { this._register(suggestController.registerSelector({ priority: 100, select: (model, pos, suggestItems) => { - transaction(tx => this.checkModelVersion(tx)); - const textModel = this.editor.getModel(); if (!textModel) { // Should not happen @@ -83,7 +78,7 @@ export class SuggestWidgetAdaptor extends Disposable { }) .filter(item => item && item.valid && item.prefixLength > 0); - const result = findFirstMaxBy( + const result = findFirstMax( candidates, compareBy(s => s!.prefixLength, numberComparator) ); @@ -142,11 +137,7 @@ export class SuggestWidgetAdaptor extends Disposable { this._isActive = newActive; this._currentSuggestItemInfo = newInlineCompletion; - transaction(tx => { - /** @description Update state from suggest widget */ - this.checkModelVersion(tx); - this._selectedItem.set(this._isActive ? this._currentSuggestItemInfo : undefined, tx); - }); + this._onDidSelectedItemChange.fire(); } } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index 648f5940596..63ab19affbe 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { getSecondaryEdits } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index e160c3daa01..e318702da15 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts index 354fa36b5af..a860898c64e 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts @@ -25,7 +25,7 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import * as assert from 'assert'; +import assert from 'assert'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; diff --git a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts index 298fcd4452e..0ded87779e8 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts @@ -16,6 +16,7 @@ import { InlineDecorationType } from 'vs/editor/common/viewModel'; import { AdditionalLinesWidget, LineData } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; import { GhostText } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ColumnRange, applyObservableDecorations } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { diffDeleteDecoration, diffLineDeleteDecorationBackgroundWithIndicator } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; export const INLINE_EDIT_DESCRIPTION = 'inline-edit'; export interface IGhostTextWidgetModel { @@ -23,12 +24,11 @@ export interface IGhostTextWidgetModel { readonly ghostText: IObservable; readonly minReservedLineCount: IObservable; readonly range: IObservable; - readonly backgroundColoring: IObservable; } export class GhostTextWidget extends Disposable { private readonly isDisposed = observableValue(this, false); - private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); + private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); constructor( private readonly editor: ICodeEditor, @@ -92,7 +92,7 @@ export class GhostTextWidget extends Disposable { let hiddenTextStartColumn: number | undefined = undefined; let lastIdx = 0; - if (!isPureRemove) { + if (!isPureRemove && (isSingleLine || !range)) { for (const part of ghostText.parts) { let lines = part.lines; //If remove range is set, we want to push all new liens to virtual area @@ -140,7 +140,6 @@ export class GhostTextWidget extends Disposable { range, isSingleLine, isPureRemove, - backgroundColoring: this.model.backgroundColoring.read(reader) }; }); @@ -164,7 +163,7 @@ export class GhostTextWidget extends Disposable { if (uiState.isSingleLine) { ranges.push(uiState.range); } - else if (uiState.isPureRemove) { + else if (!uiState.isPureRemove) { const lines = uiState.range.endLineNumber - uiState.range.startLineNumber; for (let i = 0; i < lines; i++) { const line = uiState.range.startLineNumber + i; @@ -174,24 +173,22 @@ export class GhostTextWidget extends Disposable { ranges.push(range); } } - else { - const lines = uiState.range.endLineNumber - uiState.range.startLineNumber; - for (let i = 0; i < lines; i++) { - const line = uiState.range.startLineNumber + i; - const firstNonWhitespace = uiState.targetTextModel.getLineFirstNonWhitespaceColumn(line); - const lastNonWhitespace = uiState.targetTextModel.getLineLastNonWhitespaceColumn(line); - const range = new Range(line, firstNonWhitespace, line, lastNonWhitespace); - ranges.push(range); - } - } - const className = uiState.backgroundColoring ? 'inline-edit-remove backgroundColoring' : 'inline-edit-remove'; for (const range of ranges) { decorations.push({ range, - options: { inlineClassName: className, description: 'inline-edit-remove', } + options: diffDeleteDecoration }); } } + if (uiState.range && !uiState.isSingleLine && uiState.isPureRemove) { + const r = new Range(uiState.range.startLineNumber, 1, uiState.range.endLineNumber - 1, 1); + + decorations.push({ + range: r, + options: diffLineDeleteDecorationBackgroundWithIndicator + }); + + } for (const p of uiState.inlineTexts) { @@ -215,7 +212,7 @@ export class GhostTextWidget extends Disposable { derived(reader => { /** @description lines */ const uiState = this.uiState.read(reader); - return uiState && !uiState.isPureRemove ? { + return uiState && !uiState.isPureRemove && (uiState.isSingleLine || !uiState.range) ? { lineNumber: uiState.lineNumber, additionalLines: uiState.additionalLines, minReservedLineCount: uiState.additionalReservedLineCount, diff --git a/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts b/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts index 6c1c7337f7f..cfacc77329a 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { constObservable } from 'vs/base/common/observable'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration } from 'vs/editor/common/model'; -import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { InlineEditController } from 'vs/editor/contrib/inlineEdit/browser/inlineEditController'; import { InlineEditHintsContentWidget } from 'vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget'; +import * as nls from 'vs/nls'; export class InlineEditHover implements IHoverPart { constructor( @@ -86,8 +87,8 @@ export class InlineEditHoverParticipant implements IEditorHoverParticipant { + const disposables = new DisposableStore(); this._telemetryService.publicLog2<{}, { owner: 'hediet'; @@ -97,9 +98,17 @@ export class InlineEditHoverParticipant implements IEditorHoverParticipant = { + hoverPart: hoverParts[0], + hoverElement: widgetNode, + dispose: () => disposables.dispose() + }; + return new RenderedHoverParts([renderedHoverPart]); + } - return disposableStore; + getAccessibleContent(hoverPart: InlineEditHover): string { + return nls.localize('hoverAccessibilityInlineEdits', 'There are inline edits here.'); } } diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts index 7196773a7cf..0be0d1254c0 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; +// import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { AcceptInlineEdit, JumpBackInlineEdit, JumpToInlineEdit, RejectInlineEdit, TriggerInlineEdit } from 'vs/editor/contrib/inlineEdit/browser/commands'; -import { InlineEditHoverParticipant } from 'vs/editor/contrib/inlineEdit/browser/hoverParticipant'; +// import { InlineEditHoverParticipant } from 'vs/editor/contrib/inlineEdit/browser/hoverParticipant'; import { InlineEditController } from 'vs/editor/contrib/inlineEdit/browser/inlineEditController'; registerEditorAction(AcceptInlineEdit); @@ -17,4 +17,4 @@ registerEditorAction(TriggerInlineEdit); registerEditorContribution(InlineEditController.ID, InlineEditController, EditorContributionInstantiation.Eventually); -HoverParticipantRegistry.register(InlineEditHoverParticipant); +// HoverParticipantRegistry.register(InlineEditHoverParticipant); diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css index d6d156544e0..aced5d6271e 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css @@ -6,11 +6,6 @@ .monaco-editor .inline-edit-remove { background-color: var(--vscode-editorGhostText-background); font-style: italic; - text-decoration: line-through; -} - -.monaco-editor .inline-edit-remove.backgroundColoring { - background-color: var(--vscode-diffEditor-removedLineBackground); } .monaco-editor .inline-edit-hidden { diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 4e0c10eb335..5ef1693b265 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { ISettableObservable, autorun, constObservable, disposableObservableValue, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { ISettableObservable, autorun, constObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; @@ -22,39 +22,59 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { createStyleSheet2 } from 'vs/base/browser/dom'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; - -export class InlineEditWidget implements IDisposable { - constructor(public readonly widget: GhostTextWidget, public readonly edit: IInlineEdit) { } - - dispose(): void { - this.widget.dispose(); - } -} +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { InlineEditSideBySideWidget } from 'vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { IModelService } from 'vs/editor/common/services/model'; export class InlineEditController extends Disposable { static ID = 'editor.contrib.inlineEditController'; public static readonly inlineEditVisibleKey = 'inlineEditVisible'; - public static readonly inlineEditVisibleContext = new RawContextKey(InlineEditController.inlineEditVisibleKey, false); + public static readonly inlineEditVisibleContext = new RawContextKey(this.inlineEditVisibleKey, false); private _isVisibleContext = InlineEditController.inlineEditVisibleContext.bindTo(this.contextKeyService); public static readonly cursorAtInlineEditKey = 'cursorAtInlineEdit'; - public static readonly cursorAtInlineEditContext = new RawContextKey(InlineEditController.cursorAtInlineEditKey, false); + public static readonly cursorAtInlineEditContext = new RawContextKey(this.cursorAtInlineEditKey, false); private _isCursorAtInlineEditContext = InlineEditController.cursorAtInlineEditContext.bindTo(this.contextKeyService); public static get(editor: ICodeEditor): InlineEditController | null { return editor.getContribution(InlineEditController.ID); } - private _currentEdit: ISettableObservable = this._register(disposableObservableValue(this, undefined)); + private _currentEdit: ISettableObservable = observableValue(this, undefined); + private _currentWidget = derivedDisposable(this._currentEdit, (reader) => { + const edit = this._currentEdit.read(reader); + if (!edit) { + return undefined; + } + const line = edit.range.endLineNumber; + const column = edit.range.endColumn; + const textToDisplay = edit.text.endsWith('\n') && !(edit.range.startLineNumber === edit.range.endLineNumber && edit.range.startColumn === edit.range.endColumn) ? edit.text.slice(0, -1) : edit.text; + const ghostText = new GhostText(line, [new GhostTextPart(column, textToDisplay, false)]); + //only show ghost text for single line edits + //unless it is a pure removal + //multi line edits are shown in the side by side widget + const isSingleLine = edit.range.startLineNumber === edit.range.endLineNumber && ghostText.parts.length === 1 && ghostText.parts[0].lines.length === 1; + const isPureRemoval = edit.text === ''; + if (!isSingleLine && !isPureRemoval) { + return undefined; + } + const instance = this.instantiationService.createInstance(GhostTextWidget, this.editor, { + ghostText: constObservable(ghostText), + minReservedLineCount: constObservable(0), + targetTextModel: constObservable(this.editor.getModel() ?? undefined), + range: constObservable(edit.range) + }); + return instance; + }); private _currentRequestCts: CancellationTokenSource | undefined; private _jumpBackPosition: Position | undefined; private _isAccepting: ISettableObservable = observableValue(this, false); - private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); - private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); - private readonly _backgroundColoring = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).backgroundColoring); + private readonly _enabled = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); + private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); constructor( @@ -64,6 +84,8 @@ export class InlineEditController extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, ) { super(); @@ -84,7 +106,7 @@ export class InlineEditController extends Disposable { })); //Check if the cursor is at the ghost text - const cursorPosition = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition()); + const cursorPosition = observableFromEvent(this, editor.onDidChangeCursorPosition, () => editor.getPosition()); this._register(autorun(reader => { /** @description InlineEditController.cursorPositionChanged model */ if (!this._enabled.read(reader)) { @@ -154,7 +176,8 @@ export class InlineEditController extends Disposable { }`); })); - this._register(new InlineEditHintsWidget(this.editor, this._currentEdit, this.instantiationService)); + this._register(new InlineEditHintsWidget(this.editor, this._currentWidget, this.instantiationService)); + this._register(new InlineEditSideBySideWidget(this.editor, this._currentEdit, this.instantiationService, this._diffProviderFactoryService, this._modelService)); } private checkCursorPosition(position: Position) { @@ -162,7 +185,7 @@ export class InlineEditController extends Disposable { this._isCursorAtInlineEditContext.set(false); return; } - const gt = this._currentEdit.get()?.edit; + const gt = this._currentEdit.get(); if (!gt) { this._isCursorAtInlineEditContext.set(false); return; @@ -231,17 +254,7 @@ export class InlineEditController extends Disposable { if (!edit) { return; } - const line = edit.range.endLineNumber; - const column = edit.range.endColumn; - const ghostText = new GhostText(line, [new GhostTextPart(column, edit.text, false)]); - const instance = this.instantiationService.createInstance(GhostTextWidget, this.editor, { - ghostText: constObservable(ghostText), - minReservedLineCount: constObservable(0), - targetTextModel: constObservable(this.editor.getModel() ?? undefined), - range: constObservable(edit.range), - backgroundColoring: this._backgroundColoring - }); - this._currentEdit.set(new InlineEditWidget(instance, edit), undefined); + this._currentEdit.set(edit, undefined); } public async trigger() { @@ -259,7 +272,7 @@ export class InlineEditController extends Disposable { public async accept() { this._isAccepting.set(true, undefined); - const data = this._currentEdit.get()?.edit; + const data = this._currentEdit.get(); if (!data) { return; } @@ -286,7 +299,7 @@ export class InlineEditController extends Disposable { public jumpToCurrent(): void { this._jumpBackPosition = this.editor.getSelection()?.getStartPosition(); - const data = this._currentEdit.get()?.edit; + const data = this._currentEdit.get(); if (!data) { return; } @@ -297,7 +310,7 @@ export class InlineEditController extends Disposable { } public async clear(sendRejection: boolean = true) { - const edit = this._currentEdit.get()?.edit; + const edit = this._currentEdit.get(); if (edit && edit?.rejected && sendRejection) { await this._commandService .executeCommand(edit.rejected.id, ...(edit.rejected.arguments || [])) @@ -323,11 +336,15 @@ export class InlineEditController extends Disposable { public shouldShowHoverAt(range: Range) { const currentEdit = this._currentEdit.get(); + const currentWidget = this._currentWidget.get(); if (!currentEdit) { return false; } - const edit = currentEdit.edit; - const model = currentEdit.widget.model; + if (!currentWidget) { + return false; + } + const edit = currentEdit; + const model = currentWidget.model; const overReplaceRange = Range.containsPosition(edit.range, range.getStartPosition()) || Range.containsPosition(edit.range, range.getEndPosition()); if (overReplaceRange) { return true; @@ -340,7 +357,7 @@ export class InlineEditController extends Disposable { } public shouldShowHoverAtViewZone(viewZoneId: string): boolean { - return this._currentEdit.get()?.widget.ownsViewZone(viewZoneId) ?? false; + return this._currentWidget.get()?.ownsViewZone(viewZoneId) ?? false; } } diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts index 59553805863..cb2453ab6c6 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts @@ -15,10 +15,11 @@ import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentW import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { PositionAffinity } from 'vs/editor/common/model'; -import { InlineEditWidget } from 'vs/editor/contrib/inlineEdit/browser/inlineEditController'; +import { GhostTextWidget } from 'vs/editor/contrib/inlineEdit/browser/ghostTextWidget'; import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -26,12 +27,12 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class InlineEditHintsWidget extends Disposable { - private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always'); + private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always'); private sessionPosition: Position | undefined = undefined; private readonly position = derived(this, reader => { - const ghostText = this.model.read(reader)?.widget.model.ghostText.read(reader); + const ghostText = this.model.read(reader)?.model.ghostText.read(reader); if (!this.alwaysShowToolbar.read(reader) || !ghostText || ghostText.parts.length === 0) { this.sessionPosition = undefined; @@ -50,7 +51,7 @@ export class InlineEditHintsWidget extends Disposable { constructor( private readonly editor: ICodeEditor, - private readonly model: IObservable, + private readonly model: IObservable, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -202,9 +203,10 @@ export class CustomizedMenuWorkbenchToolBar extends WorkbenchToolBar { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, + @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, telemetryService); + super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); this._store.add(this.menu.onDidChange(() => this.updateToolbar())); this._store.add(this.editor.onDidChangeCursorPosition(() => this.updateToolbar())); diff --git a/extensions/microsoft-authentication/src/node/crypto.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css similarity index 58% rename from extensions/microsoft-authentication/src/node/crypto.ts rename to src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css index 1b45ad993c3..bc7e553e4d8 100644 --- a/extensions/microsoft-authentication/src/node/crypto.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css @@ -2,6 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nodeCrypto from 'crypto'; -export const crypto: Crypto = nodeCrypto.webcrypto as any; +.monaco-editor .inlineEditSideBySide { + z-index: 39; + color: var(--vscode-editorHoverWidget-foreground); + background-color: var(--vscode-editorHoverWidget-background); + border: 1px solid var(--vscode-editorHoverWidget-border); + white-space: pre; +} diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts new file mode 100644 index 00000000000..777a8b15a20 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts @@ -0,0 +1,355 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ObservablePromise, autorun, autorunWithStore, derived, observableSignalFromEvent } from 'vs/base/common/observable'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./inlineEditSideBySideWidget'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { IInlineEdit } from 'vs/editor/common/languages'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +function* range(start: number, end: number, step = 1) { + if (end === undefined) { [end, start] = [start, 0]; } + for (let n = start; n < end; n += step) { yield n; } +} + +function removeIndentation(lines: string[]) { + const indentation = lines[0].match(/^\s*/)?.[0] ?? ''; + const length = indentation.length; + + return { + text: lines.map(l => l.replace(new RegExp('^' + indentation), '')), + shift: length + }; +} + +type Pos = { + top: number; + left: Position; +}; + +export class InlineEditSideBySideWidget extends Disposable { + private static _modelId = 0; + private static _createUniqueUri(): URI { + return URI.from({ scheme: 'inline-edit-widget', path: new Date().toString() + String(InlineEditSideBySideWidget._modelId++) }); + } + + private readonly _position = derived(this, reader => { + const ghostText = this._model.read(reader); + + if (!ghostText || ghostText.text.length === 0) { + return null; + } + if (ghostText.range.startLineNumber === ghostText.range.endLineNumber && !(ghostText.range.startColumn === ghostText.range.endColumn && ghostText.range.startColumn === 1)) { + //for inner-line suggestions we still want to use minimal ghost text + return null; + } + const editorModel = this._editor.getModel(); + if (!editorModel) { + return null; + } + const lines = Array.from(range(ghostText.range.startLineNumber, ghostText.range.endLineNumber + 1)); + const lengths = lines.map(lineNumber => editorModel.getLineLastNonWhitespaceColumn(lineNumber)); + const maxColumn = Math.max(...lengths); + const lineOfMaxColumn = lines[lengths.indexOf(maxColumn)]; + + const position = new Position(lineOfMaxColumn, maxColumn); + const pos = { + top: ghostText.range.startLineNumber, + left: position + }; + + return pos; + }); + + private readonly _text = derived(this, reader => { + const ghostText = this._model.read(reader); + if (!ghostText) { + return { text: '', shift: 0 }; + } + const t = removeIndentation(ghostText.text.split('\n')); + return { + text: t.text.join('\n'), + shift: t.shift + }; + }); + + + private readonly _originalModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditSideBySideWidget._createUniqueUri())).keepObserved(this._store); + private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditSideBySideWidget._createUniqueUri())).keepObserved(this._store); + + private readonly _diff = derived(this, reader => { + return this._diffPromise.read(reader)?.promiseResult.read(reader)?.data; + }); + + private readonly _diffPromise = derived(this, reader => { + const ghostText = this._model.read(reader); + if (!ghostText) { + return; + } + const editorModel = this._editor.getModel(); + if (!editorModel) { + return; + } + const originalText = removeIndentation(editorModel.getValueInRange(ghostText.range).split('\n')).text.join('\n'); + const modifiedText = removeIndentation(ghostText.text.split('\n')).text.join('\n'); + this._originalModel.get().setValue(originalText); + this._modifiedModel.get().setValue(modifiedText); + const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); + return ObservablePromise.fromFn(async () => { + const result = await d.computeDiff(this._originalModel.get(), this._modifiedModel.get(), { + computeMoves: false, + ignoreTrimWhitespace: false, + maxComputationTimeMs: 1000, + }, CancellationToken.None); + + if (result.identical) { + return undefined; + } + + return result.changes; + }); + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _model: IObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._register(autorunWithStore((reader, store) => { + /** @description setup content widget */ + const model = this._model.read(reader); + if (!model) { + return; + } + if (this._position.get() === null) { + return; + } + const contentWidget = store.add(this._instantiationService.createInstance( + InlineEditSideBySideContentWidget, + this._editor, + this._position, + this._text.map(t => t.text), + this._text.map(t => t.shift), + this._diff + )); + _editor.addOverlayWidget(contentWidget); + store.add(toDisposable(() => _editor.removeOverlayWidget(contentWidget))); + })); + } +} + +class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWidget { + private static _dropDownVisible = false; + public static get dropDownVisible() { return this._dropDownVisible; } + + private static id = 0; + + private readonly id = `InlineEditSideBySideContentWidget${InlineEditSideBySideContentWidget.id++}`; + public readonly allowEditorOverflow = false; + + private readonly _nodes = $('div.inlineEditSideBySide', undefined); + + private readonly _scrollChanged = observableSignalFromEvent('editor.onDidScrollChange', this._editor.onDidScrollChange); + + private readonly _previewEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._nodes, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + scrollbar: { vertical: 'hidden', horizontal: 'hidden', alwaysConsumeMouseWheel: false, handleMouseWheel: false }, + readOnly: true, + wordWrap: 'off', + wordWrapOverride1: 'off', + wordWrapOverride2: 'off', + wrappingIndent: 'none', + wrappingStrategy: undefined, + }, + { contributions: [], isSimpleWidget: true }, + this._editor + )); + + private readonly _previewEditorObs = observableCodeEditor(this._previewEditor); + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _previewTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + this._editor.getModel()?.getLanguageId() ?? PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + + private readonly _setText = derived(reader => { + const edit = this._text.read(reader); + if (!edit) { return; } + this._previewTextModel.setValue(edit); + }).recomputeInitiallyAndOnChange(this._store); + + + private readonly _decorations = derived(this, (reader) => { + this._setText.read(reader); + const position = this._position.read(reader); + if (!position) { return { org: [], mod: [] }; } + const diff = this._diff.read(reader); + if (!diff) { return { org: [], mod: [] }; } + + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + if (diff.length === 1 && diff[0].innerChanges![0].modifiedRange.equalsRange(this._previewTextModel.getFullModelRange())) { + return { org: [], mod: [] }; + } + const shift = this._shift.get(); + + const moveRange = (range: IRange) => { + return new Range(range.startLineNumber + position.top - 1, range.startColumn + shift, range.endLineNumber + position.top - 1, range.endColumn + shift); + }; + + for (const m of diff) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: moveRange(m.original.toInclusiveRange()!), options: diffLineDeleteDecorationBackgroundWithIndicator }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator }); + } + + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: moveRange(m.original.toInclusiveRange()!), options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber)) { + originalDecorations.push({ range: moveRange(i.originalRange), options: i.originalRange.isEmpty() ? diffDeleteDecorationEmpty : diffDeleteDecoration }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ range: i.modifiedRange, options: i.modifiedRange.isEmpty() ? diffAddDecorationEmpty : diffAddDecoration }); + } + } + } + } + + return { org: originalDecorations, mod: modifiedDecorations }; + }); + + private readonly _originalDecorations = derived(this, reader => { + return this._decorations.read(reader).org; + }); + + private readonly _modifiedDecorations = derived(this, reader => { + return this._decorations.read(reader).mod; + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _position: IObservable, + private readonly _text: IObservable, + private readonly _shift: IObservable, + private readonly _diff: IObservable, + + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._previewEditor.setModel(this._previewTextModel); + + this._register(this._editorObs.setDecorations(this._originalDecorations)); + this._register(this._previewEditorObs.setDecorations(this._modifiedDecorations)); + + this._register(autorun(reader => { + const width = this._previewEditorObs.contentWidth.read(reader); + const lines = this._text.read(reader).split('\n').length - 1; + const height = this._editor.getOption(EditorOption.lineHeight) * lines; + if (width <= 0) { + return; + } + this._previewEditor.layout({ height: height, width: width }); + })); + + this._register(autorun(reader => { + /** @description update position */ + this._position.read(reader); + this._editor.layoutOverlayWidget(this); + })); + + this._register(autorun(reader => { + /** @description scroll change */ + this._scrollChanged.read(reader); + const position = this._position.read(reader); + if (!position) { + return; + } + this._editor.layoutOverlayWidget(this); + })); + } + + getId(): string { return this.id; } + + getDomNode(): HTMLElement { + return this._nodes; + } + + getPosition(): IOverlayWidgetPosition | null { + const position = this._position.get(); + if (!position) { + return null; + } + const layoutInfo = this._editor.getLayoutInfo(); + const visibPos = this._editor.getScrolledVisiblePosition(new Position(position.top, 1)); + if (!visibPos) { + return null; + } + const top = visibPos.top - 1; //-1 to offset the border width + const offset = this._editor.getOffsetForColumn(position.left.lineNumber, position.left.column); + const left = layoutInfo.contentLeft + offset + 10; + return { + preference: { + left, + top, + } + }; + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/commands.ts b/src/vs/editor/contrib/inlineEdits/browser/commands.ts new file mode 100644 index 00000000000..ec7255fd9f0 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/commands.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { transaction } from 'vs/base/common/observable'; +import { asyncTransaction } from 'vs/base/common/observableInternal/base'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { inlineEditAcceptId, inlineEditVisible, showNextInlineEditActionId, showPreviousInlineEditActionId } from 'vs/editor/contrib/inlineEdits/browser/consts'; +import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController'; +import * as nls from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; + + +function labelAndAlias(str: nls.ILocalizedString): { label: string; alias: string } { + return { + label: str.value, + alias: str.original, + }; +} + +export class ShowNextInlineEditAction extends EditorAction { + public static ID = showNextInlineEditActionId; + constructor() { + super({ + id: ShowNextInlineEditAction.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.showNext', "Show Next Inline Edit")), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible), + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.BracketRight, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + controller?.model.get()?.next(); + } +} + +export class ShowPreviousInlineEditAction extends EditorAction { + public static ID = showPreviousInlineEditActionId; + constructor() { + super({ + id: ShowPreviousInlineEditAction.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.showPrevious', "Show Previous Inline Edit")), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible), + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.BracketLeft, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + controller?.model.get()?.previous(); + } +} + +export class TriggerInlineEditAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineEdits.trigger', + ...labelAndAlias(nls.localize2('action.inlineEdits.trigger', "Trigger Inline Edit")), + precondition: EditorContextKeys.writable + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + await asyncTransaction(async tx => { + /** @description triggerExplicitly from command */ + await controller?.model.get()?.triggerExplicitly(tx); + }); + } +} + +export class AcceptInlineEdit extends EditorAction { + constructor() { + super({ + id: inlineEditAcceptId, + ...labelAndAlias(nls.localize2('action.inlineEdits.accept', "Accept Inline Edit")), + precondition: inlineEditVisible, + menuOpts: { + menuId: MenuId.InlineEditsActions, + title: nls.localize('inlineEditsActions', "Accept Inline Edit"), + group: 'primary', + order: 1, + icon: Codicon.check, + }, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.Space, + weight: 20000, + kbExpr: inlineEditVisible, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + if (editor instanceof EmbeddedCodeEditorWidget) { + editor = editor.getParentEditor(); + } + const controller = InlineEditsController.get(editor); + if (controller) { + controller.model.get()?.accept(controller.editor); + controller.editor.focus(); + } + } +} + +/* +TODO@hediet +export class PinInlineEdit extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineEdits.pin', + ...labelAndAlias(nls.localize2('action.inlineEdits.pin', "Pin Inline Edit")), + precondition: undefined, + kbOpts: { + primary: KeyMod.Shift | KeyCode.Space, + weight: 20000, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + if (controller) { + controller.model.get()?.togglePin(); + } + } +} + +MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, { + command: { + id: 'editor.action.inlineEdits.pin', + title: nls.localize('Pin', "Pin"), + icon: Codicon.pin, + }, + group: 'primary', + order: 1, + when: isPinnedContextKey.negate(), +}); + +MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, { + command: { + id: 'editor.action.inlineEdits.unpin', + title: nls.localize('Unpin', "Unpin"), + icon: Codicon.pinned, + }, + group: 'primary', + order: 1, + when: isPinnedContextKey, +});*/ + +export class HideInlineEdit extends EditorAction { + public static ID = 'editor.action.inlineEdits.hide'; + + constructor() { + super({ + id: HideInlineEdit.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.hide', "Hide Inline Edit")), + precondition: inlineEditVisible, + kbOpts: { + weight: 100, + primary: KeyCode.Escape, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + transaction(tx => { + controller?.model.get()?.stop(tx); + }); + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/consts.ts b/src/vs/editor/contrib/inlineEdits/browser/consts.ts new file mode 100644 index 00000000000..9ad19e98a76 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/consts.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 { localize } from 'vs/nls'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const inlineEditAcceptId = 'editor.action.inlineEdits.accept'; + +export const showPreviousInlineEditActionId = 'editor.action.inlineEdits.showPrevious'; + +export const showNextInlineEditActionId = 'editor.action.inlineEdits.showNext'; + +export const inlineEditVisible = new RawContextKey('inlineEditsVisible', false, localize('inlineEditsVisible', "Whether an inline edit is visible")); +export const isPinnedContextKey = new RawContextKey('inlineEditsIsPinned', false, localize('isPinned', "Whether an inline edit is visible")); diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts new file mode 100644 index 00000000000..ae8b7182a89 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { + TriggerInlineEditAction, ShowNextInlineEditAction, ShowPreviousInlineEditAction, + AcceptInlineEdit, HideInlineEdit, +} from 'vs/editor/contrib/inlineEdits/browser/commands'; +import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController'; + +registerEditorContribution(InlineEditsController.ID, InlineEditsController, EditorContributionInstantiation.Eventually); + +registerEditorAction(TriggerInlineEditAction); +registerEditorAction(ShowNextInlineEditAction); +registerEditorAction(ShowPreviousInlineEditAction); +registerEditorAction(AcceptInlineEdit); +registerEditorAction(HideInlineEdit); diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts new file mode 100644 index 00000000000..9055ec56719 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.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 'vs/base/common/lifecycle'; +import { derived, derivedObservableWithCache, IReader, ISettableObservable, observableValue } from 'vs/base/common/observable'; +import { derivedDisposable, derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { inlineEditVisible, isPinnedContextKey } from 'vs/editor/contrib/inlineEdits/browser/consts'; +import { InlineEditsModel } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsModel'; +import { InlineEditsWidget } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; + +export class InlineEditsController extends Disposable { + static ID = 'editor.contrib.inlineEditsController'; + + public static get(editor: ICodeEditor): InlineEditsController | null { + return editor.getContribution(InlineEditsController.ID); + } + + private readonly _enabled = observableConfigValue('editor.inlineEdits.enabled', false, this._configurationService); + private readonly _editorObs = observableCodeEditor(this.editor); + private readonly _selection = derived(this, reader => this._editorObs.cursorSelection.read(reader) ?? new Selection(1, 1, 1, 1)); + + private readonly _debounceValue = this._debounceService.for( + this._languageFeaturesService.inlineCompletionsProvider, + 'InlineEditsDebounce', + { min: 50, max: 50 } + ); + + public readonly model = derivedDisposable(this, reader => { + if (!this._enabled.read(reader)) { + return undefined; + } + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineEditsModel = this._instantiationService.createInstance( + readHotReloadableExport(InlineEditsModel, reader), + textModel, + this._editorObs.versionId, + this._selection, + this._debounceValue, + ); + + return model; + }); + + private readonly _hadInlineEdit = derivedObservableWithCache(this, (reader, lastValue) => lastValue || this.model.read(reader)?.inlineEdit.read(reader) !== undefined); + + protected readonly _widget = derivedDisposable(this, reader => { + if (!this._hadInlineEdit.read(reader)) { return undefined; } + + return this._instantiationService.createInstance( + readHotReloadableExport(InlineEditsWidget, reader), + this.editor, + this.model.map((m, reader) => m?.inlineEdit.read(reader)), + flattenSettableObservable((reader) => this.model.read(reader)?.userPrompt ?? observableValue('empty', '')), + ); + }); + + constructor( + public readonly editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILanguageFeatureDebounceService private readonly _debounceService: ILanguageFeatureDebounceService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + + this._register(bindContextKey(inlineEditVisible, this._contextKeyService, r => !!this.model.read(r)?.inlineEdit.read(r))); + this._register(bindContextKey(isPinnedContextKey, this._contextKeyService, r => !!this.model.read(r)?.isPinned.read(r))); + + this.model.recomputeInitiallyAndOnChange(this._store); + this._widget.recomputeInitiallyAndOnChange(this._store); + } +} + +function flattenSettableObservable(fn: (reader: IReader | undefined) => ISettableObservable): ISettableObservable { + return derivedWithSetter(undefined, reader => { + const obs = fn(reader); + return obs.read(reader); + }, (value, tx) => { + fn(undefined).set(value, tx); + }); +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts new file mode 100644 index 00000000000..812818c85b1 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts @@ -0,0 +1,289 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { timeout } from 'vs/base/common/async'; +import { CancellationToken, cancelOnDispose } from 'vs/base/common/cancellation'; +import { itemsEquals, structuralEquals } from 'vs/base/common/equals'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from 'vs/base/common/observable'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Command, InlineCompletionContext, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions'; +import { InlineEdit } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget'; + +export class InlineEditsModel extends Disposable { + private static _modelId = 0; + private static _createUniqueUri(): URI { + return URI.from({ scheme: 'inline-edits', path: new Date().toString() + String(InlineEditsModel._modelId++) }); + } + + private readonly _forceUpdateExplicitlySignal = observableSignal(this); + + // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. + private readonly _selectedInlineCompletionId = observableValue(this, undefined); + + private readonly _isActive = observableValue(this, false); + + private readonly _originalModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store); + private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store); + + private readonly _pinnedRange = new TrackedRange(this.textModel, this._textModelVersionId); + + public readonly isPinned = this._pinnedRange.range.map(range => !!range); + + public readonly userPrompt: ISettableObservable = observableValue(this, undefined); + + constructor( + public readonly textModel: ITextModel, + public readonly _textModelVersionId: IObservable, + private readonly _selection: IObservable, + protected readonly _debounceValue: IFeatureDebounceInformation, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._register(recomputeInitiallyAndOnChange(this._fetchInlineEditsPromise)); + } + + public readonly inlineEdit = derived(this, reader => { + return this._inlineEdit.read(reader)?.promiseResult.read(reader)?.data; + }); + + public readonly _inlineEdit = derived | undefined>(this, reader => { + const edit = this.selectedInlineEdit.read(reader); + if (!edit) { return undefined; } + const range = edit.inlineCompletion.range; + if (edit.inlineCompletion.insertText.trim() === '') { + return undefined; + } + + let newLines = edit.inlineCompletion.insertText.split(/\r\n|\r|\n/); + + function removeIndentation(lines: string[]): string[] { + const indentation = lines[0].match(/^\s*/)?.[0] ?? ''; + return lines.map(l => l.replace(new RegExp('^' + indentation), '')); + } + newLines = removeIndentation(newLines); + + const existing = this.textModel.getValueInRange(range); + let existingLines = existing.split(/\r\n|\r|\n/); + existingLines = removeIndentation(existingLines); + this._originalModel.get().setValue(existingLines.join('\n')); + this._modifiedModel.get().setValue(newLines.join('\n')); + + const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); + return ObservablePromise.fromFn(async () => { + const result = await d.computeDiff(this._originalModel.get(), this._modifiedModel.get(), { + computeMoves: false, + ignoreTrimWhitespace: false, + maxComputationTimeMs: 1000, + }, CancellationToken.None); + + if (result.identical) { + return undefined; + } + + return new InlineEdit(LineRange.fromRangeInclusive(range), removeIndentation(newLines), result.changes); + }); + }); + + private readonly _fetchStore = this._register(new DisposableStore()); + + private readonly _inlineEditsFetchResult = disposableObservableValue(this, undefined); + private readonly _inlineEdits = derivedOpts({ owner: this, equalsFn: structuralEquals }, reader => { + return this._inlineEditsFetchResult.read(reader)?.completions.map(c => new InlineEditData(c)) ?? []; + }); + + private readonly _fetchInlineEditsPromise = derivedHandleChanges({ + owner: this, + createEmptyChangeSummary: () => ({ + inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic + }), + handleChange: (ctx, changeSummary) => { + /** @description fetch inline completions */ + if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; + } + return true; + }, + }, async (reader, changeSummary) => { + this._fetchStore.clear(); + this._forceUpdateExplicitlySignal.read(reader); + /*if (!this._isActive.read(reader)) { + return undefined; + }*/ + this._textModelVersionId.read(reader); + + function mapValue(value: T, fn: (value: T) => TOut): TOut { + return fn(value); + } + + const selection = this._pinnedRange.range.read(reader) ?? mapValue(this._selection.read(reader), v => v.isEmpty() ? undefined : v); + if (!selection) { + this._inlineEditsFetchResult.set(undefined, undefined); + this.userPrompt.set(undefined, undefined); + return undefined; + } + const context: InlineCompletionContext = { + triggerKind: changeSummary.inlineCompletionTriggerKind, + selectedSuggestionInfo: undefined, + userPrompt: this.userPrompt.read(reader), + }; + + const token = cancelOnDispose(this._fetchStore); + await timeout(200, token); + const result = await provideInlineCompletions(this.languageFeaturesService.inlineCompletionsProvider, selection, this.textModel, context, token); + if (token.isCancellationRequested) { + return; + } + + this._inlineEditsFetchResult.set(result, undefined); + }); + + public async trigger(tx?: ITransaction): Promise { + this._isActive.set(true, tx); + await this._fetchInlineEditsPromise.get(); + } + + public async triggerExplicitly(tx?: ITransaction): Promise { + subtransaction(tx, tx => { + this._isActive.set(true, tx); + this._forceUpdateExplicitlySignal.trigger(tx); + }); + await this._fetchInlineEditsPromise.get(); + } + + public stop(tx?: ITransaction): void { + subtransaction(tx, tx => { + this.userPrompt.set(undefined, tx); + this._isActive.set(false, tx); + this._inlineEditsFetchResult.set(undefined, tx); + this._pinnedRange.setRange(undefined, tx); + //this._source.clear(tx); + }); + } + + private readonly _filteredInlineEditItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + return this._inlineEdits.read(reader); + }); + + public readonly selectedInlineCompletionIndex = derived(this, (reader) => { + const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader); + const filteredCompletions = this._filteredInlineEditItems.read(reader); + const idx = this._selectedInlineCompletionId === undefined ? -1 + : filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId); + if (idx === -1) { + // Reset the selection so that the selection does not jump back when it appears again + this._selectedInlineCompletionId.set(undefined, undefined); + return 0; + } + return idx; + }); + + public readonly selectedInlineEdit = derived(this, (reader) => { + const filteredCompletions = this._filteredInlineEditItems.read(reader); + const idx = this.selectedInlineCompletionIndex.read(reader); + return filteredCompletions[idx]; + }); + + public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, + r => this.selectedInlineEdit.read(r)?.inlineCompletion.source.inlineCompletions.commands ?? [] + ); + + private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise { + await this.triggerExplicitly(); + + const completions = this._filteredInlineEditItems.get() || []; + if (completions.length > 0) { + const newIdx = (this.selectedInlineCompletionIndex.get() + delta + completions.length) % completions.length; + this._selectedInlineCompletionId.set(completions[newIdx].semanticId, undefined); + } else { + this._selectedInlineCompletionId.set(undefined, undefined); + } + } + + public async next(): Promise { + await this._deltaSelectedInlineCompletionIndex(1); + } + + public async previous(): Promise { + await this._deltaSelectedInlineCompletionIndex(-1); + } + + public togglePin(): void { + if (this.isPinned.get()) { + this._pinnedRange.setRange(undefined, undefined); + } else { + this._pinnedRange.setRange(this._selection.get(), undefined); + } + } + + public async accept(editor: ICodeEditor): Promise { + if (editor.getModel() !== this.textModel) { + throw new BugIndicatingError(); + } + const edit = this.selectedInlineEdit.get(); + if (!edit) { + return; + } + + editor.pushUndoStop(); + editor.executeEdits( + 'inlineSuggestion.accept', + [ + edit.inlineCompletion.toSingleTextEdit().toSingleEditOperation() + ] + ); + this.stop(); + } +} + +class InlineEditData { + public readonly semanticId = this.inlineCompletion.hash(); + + constructor(public readonly inlineCompletion: InlineCompletionItem) { + + } +} + +class TrackedRange extends Disposable { + private readonly _decorations = observableValue(this, []); + + constructor( + private readonly _textModel: ITextModel, + private readonly _versionId: IObservable, + ) { + super(); + this._register(toDisposable(() => { + this._textModel.deltaDecorations(this._decorations.get(), []); + })); + } + + setRange(range: Range | undefined, tx: ITransaction | undefined): void { + this._decorations.set(this._textModel.deltaDecorations(this._decorations.get(), range ? [{ range, options: { description: 'trackedRange' } }] : []), tx); + } + + public readonly range = derived(this, reader => { + this._versionId.read(reader); + const deco = this._decorations.read(reader)[0]; + if (!deco) { return null; } + + return this._textModel.getDecorationRange(deco) ?? null; + }); +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css new file mode 100644 index 00000000000..68910c883a6 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor div.inline-edits-widget { + --widget-color: var(--vscode-notifications-background); + + .promptEditor .monaco-editor { + --vscode-editor-placeholder-foreground: var(--vscode-editorGhostText-foreground); + } + + .toolbar, .promptEditor { + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + &:hover, &.focused { + .toolbar, .promptEditor { + opacity: 1; + } + } + + .preview .monaco-editor { + + .mtk1 { + /*color: rgba(215, 215, 215, 0.452);*/ + color: var(--vscode-editorGhostText-foreground); + } + .view-overlays .current-line-exact { + border: none; + } + + .current-line-margin { + border: none; + } + + --vscode-editor-background: var(--widget-color); + } + + svg { + .gradient-start { + stop-color: var(--vscode-editor-background); + } + + .gradient-stop { + stop-color: var(--widget-color); + } + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts new file mode 100644 index 00000000000..331520cffe0 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts @@ -0,0 +1,400 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h, svgElem } from 'vs/base/browser/dom'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { autorun, constObservable, derived, IObservable, ISettableObservable } from 'vs/base/common/observable'; +import { derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import 'vs/css!./inlineEditsWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; +import { appendRemoveOnDispose, applyStyle } from 'vs/editor/browser/widget/diffEditor/utils'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; +import { PlaceholderTextContribution } from '../../placeholderText/browser/placeholderTextContribution'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class InlineEdit { + constructor( + public readonly range: LineRange, + public readonly newLines: string[], + public readonly changes: readonly DetailedLineRangeMapping[], + ) { + + } +} + +export class InlineEditsWidget extends Disposable { + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _elements = h('div.inline-edits-widget', { + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + }, + }, [ + h('div@editorContainer', { style: { position: 'absolute', top: '0px', left: '0px', width: '500px', height: '500px', } }, [ + h('div.toolbar@toolbar', { style: { position: 'absolute', top: '-25px', left: '0px' } }), + h('div.promptEditor@promptEditor', { style: { position: 'absolute', top: '-25px', left: '80px', width: '300px', height: '22px' } }), + h('div.preview@editor', { style: { position: 'absolute', top: '0px', left: '0px' } }), + ]), + svgElem('svg', { style: { overflow: 'visible', pointerEvents: 'none' }, }, [ + svgElem('defs', [ + svgElem('linearGradient', { + id: 'Gradient2', + x1: '0', + y1: '0', + x2: '1', + y2: '0', + }, [ + /*svgElem('stop', { offset: '0%', class: 'gradient-start', }), + svgElem('stop', { offset: '0%', class: 'gradient-start', }), + svgElem('stop', { offset: '20%', class: 'gradient-stop', }),*/ + svgElem('stop', { offset: '0%', class: 'gradient-stop', }), + svgElem('stop', { offset: '100%', class: 'gradient-stop', }), + ]), + ]), + svgElem('path@path', { + d: '', + fill: 'url(#Gradient2)', + }), + ]), + ]); + + protected readonly _toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar, MenuId.InlineEditsActions, { + toolbarOptions: { + primaryGroup: g => g.startsWith('primary'), + }, + })); + private readonly _previewTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + + private readonly _setText = derived(reader => { + const edit = this._edit.read(reader); + if (!edit) { return; } + this._previewTextModel.setValue(edit.newLines.join('\n')); + }).recomputeInitiallyAndOnChange(this._store); + + + private readonly _promptTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + private readonly _promptEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._elements.promptEditor, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + placeholder: 'Describe the change you want...', + fontFamily: DEFAULT_FONT_FAMILY, + }, + { + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SuggestController.ID, + PlaceholderTextContribution.ID, + ContextMenuController.ID, + ]), + isSimpleWidget: true + }, + this._editor + )); + + private readonly _previewEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._elements.editor, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + }, + { contributions: [], }, + this._editor + )); + + private readonly _previewEditorObs = observableCodeEditor(this._previewEditor); + + private readonly _decorations = derived(this, (reader) => { + this._setText.read(reader); + const diff = this._edit.read(reader)?.changes; + if (!diff) { return []; } + + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + if (diff.length === 1 && diff[0].innerChanges![0].modifiedRange.equalsRange(this._previewTextModel.getFullModelRange())) { + return []; + } + + for (const m of diff) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffLineDeleteDecorationBackgroundWithIndicator }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator }); + } + + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber)) { + originalDecorations.push({ range: i.originalRange, options: i.originalRange.isEmpty() ? diffDeleteDecorationEmpty : diffDeleteDecoration }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ range: i.modifiedRange, options: i.modifiedRange.isEmpty() ? diffAddDecorationEmpty : diffAddDecoration }); + } + } + } + } + + return modifiedDecorations; + }); + + private readonly _layout1 = derived(this, reader => { + const model = this._editor.getModel()!; + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const range = inlineEdit.range; + + let maxLeft = 0; + for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { + const column = model.getLineMaxColumn(i); + const left = this._editor.getOffsetForColumn(i, column); + maxLeft = Math.max(maxLeft, left); + } + + const layoutInfo = this._editor.getLayoutInfo(); + const contentLeft = layoutInfo.contentLeft; + + return { left: contentLeft + maxLeft }; + }); + + private readonly _layout = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const range = inlineEdit.range; + + const scrollLeft = this._editorObs.scrollLeft.read(reader); + + const left = this._layout1.read(reader)!.left + 20 - scrollLeft; + + const selectionTop = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + + const topCode = new Point(left, selectionTop); + const bottomCode = new Point(left, selectionBottom); + const codeHeight = selectionBottom - selectionTop; + + const codeEditDist = 50; + const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.newLines.length; + const difference = codeHeight - editHeight; + const topEdit = new Point(left + codeEditDist, selectionTop + (difference / 2)); + const bottomEdit = new Point(left + codeEditDist, selectionBottom - (difference / 2)); + + return { + topCode, + bottomCode, + codeHeight, + topEdit, + bottomEdit, + editHeight, + }; + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _edit: IObservable, + private readonly _userPrompt: ISettableObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + const visible = derived(this, reader => this._edit.read(reader) !== undefined || this._userPrompt.read(reader) !== undefined); + this._register(applyStyle(this._elements.root, { + display: derived(this, reader => visible.read(reader) ? 'block' : 'none') + })); + + this._register(appendRemoveOnDispose(this._editor.getDomNode()!, this._elements.root)); + + this._register(observableCodeEditor(_editor).createOverlayWidget({ + domNode: this._elements.root, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const x = this._layout1.read(reader)?.left; + if (x === undefined) { return 0; } + const width = this._previewEditorObs.contentWidth.read(reader); + return x + width; + }), + })); + + this._previewEditor.setModel(this._previewTextModel); + + this._register(this._previewEditorObs.setDecorations(this._decorations)); + + this._register(autorun(reader => { + const layoutInfo = this._layout.read(reader); + if (!layoutInfo) { return; } + + const { topCode, bottomCode, topEdit, bottomEdit, editHeight } = layoutInfo; + + const straightWidthCode = 10; + const straightWidthEdit = 0; + const bezierDist = 40; + + const path = new PathBuilder() + .moveTo(topCode) + .lineTo(topCode.deltaX(straightWidthCode)) + .curveTo( + topCode.deltaX(straightWidthCode + bezierDist), + topEdit.deltaX(-bezierDist - straightWidthEdit), + topEdit.deltaX(-straightWidthEdit), + ) + .lineTo(topEdit) + .lineTo(bottomEdit) + .lineTo(bottomEdit.deltaX(-straightWidthEdit)) + .curveTo( + bottomEdit.deltaX(-bezierDist - straightWidthEdit), + bottomCode.deltaX(straightWidthCode + bezierDist), + bottomCode.deltaX(straightWidthCode), + ) + .lineTo(bottomCode) + .build(); + + + this._elements.path.setAttribute('d', path); + + this._elements.editorContainer.style.top = `${topEdit.y}px`; + this._elements.editorContainer.style.left = `${topEdit.x}px`; + this._elements.editorContainer.style.height = `${editHeight}px`; + + const width = this._previewEditorObs.contentWidth.read(reader); + this._previewEditor.layout({ height: editHeight, width }); + })); + + this._promptEditor.setModel(this._promptTextModel); + this._promptEditor.layout(); + this._register(createTwoWaySync(mapSettableObservable(this._userPrompt, v => v ?? '', v => v), observableCodeEditor(this._promptEditor).value)); + + this._register(autorun(reader => { + const isFocused = observableCodeEditor(this._promptEditor).isFocused.read(reader); + this._elements.root.classList.toggle('focused', isFocused); + })); + } +} + +function mapSettableObservable(obs: ISettableObservable, fn1: (value: T) => T1, fn2: (value: T1) => T): ISettableObservable { + return derivedWithSetter(undefined, reader => fn1(obs.read(reader)), (value, tx) => obs.set(fn2(value), tx)); +} + +class Point { + constructor( + public readonly x: number, + public readonly y: number, + ) { } + + public add(other: Point): Point { + return new Point(this.x + other.x, this.y + other.y); + } + + public deltaX(delta: number): Point { + return new Point(this.x + delta, this.y); + } +} + +class PathBuilder { + private _data: string = ''; + + public moveTo(point: Point): this { + this._data += `M ${point.x} ${point.y} `; + return this; + } + + public lineTo(point: Point): this { + this._data += `L ${point.x} ${point.y} `; + return this; + } + + public curveTo(cp1: Point, cp2: Point, to: Point): this { + this._data += `C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y} ${to.x} ${to.y} `; + return this; + } + + public build(): string { + return this._data; + } +} + +function createTwoWaySync(main: ISettableObservable, target: ISettableObservable): IDisposable { + const store = new DisposableStore(); + store.add(autorun(reader => { + const value = main.read(reader); + target.set(value, undefined); + })); + store.add(autorun(reader => { + const value = target.read(reader); + main.set(value, undefined); + })); + return store; +} diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts index db892ea676c..25f273c1262 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { CancelablePromise, disposableTimeout } from 'vs/base/common/async'; +import { disposableTimeout } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { noBreakWhitespace } from 'vs/base/common/strings'; @@ -114,13 +114,13 @@ export class InlineProgressManager extends Disposable { private readonly _showPromise = this._register(new MutableDisposable()); private readonly _currentDecorations: IEditorDecorationsCollection; - private readonly _currentWidget = new MutableDisposable(); + private readonly _currentWidget = this._register(new MutableDisposable()); private _operationIdPool = 0; private _currentOperation?: number; constructor( - readonly id: string, + private readonly id: string, private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { @@ -129,7 +129,12 @@ export class InlineProgressManager extends Disposable { this._currentDecorations = _editor.createDecorationsCollection(); } - public async showWhile(position: IPosition, title: string, promise: CancelablePromise): Promise { + public override dispose(): void { + super.dispose(); + this._currentDecorations.clear(); + } + + public async showWhile(position: IPosition, title: string, promise: Promise, delegate: InlineProgressDelegate, delayOverride?: number): Promise { const operationId = this._operationIdPool++; this._currentOperation = operationId; @@ -143,9 +148,9 @@ export class InlineProgressManager extends Disposable { }]); if (decorationIds.length > 0) { - this._currentWidget.value = this._instantiationService.createInstance(InlineProgressWidget, this.id, this._editor, range, title, promise); + this._currentWidget.value = this._instantiationService.createInstance(InlineProgressWidget, this.id, this._editor, range, title, delegate); } - }, this._showDelay); + }, delayOverride ?? this._showDelay); try { return await promise; diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css b/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css index 105c60d52b5..cb376acc002 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css @@ -25,5 +25,6 @@ } .inline-progress-widget:hover .icon::before { - content: "\ea76"; /* codicon-x */ + content: var(--vscode-icon-x-content); + font-family: var(--vscode-icon-x-font-family); } diff --git a/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts b/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts index 4ef8e7d2cfc..17b47434db7 100644 --- a/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts +++ b/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction } from 'vs/editor/browser/editorExtensions'; diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 45b3fafba71..60601d1fcdd 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -11,6 +11,7 @@ import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandTha import { TrimTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { EnterOperation } from 'vs/editor/common/cursor/cursorTypeEditOperations'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -594,7 +595,7 @@ export class InsertLineBeforeAction extends EditorAction { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, EnterOperation.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } @@ -619,7 +620,7 @@ export class InsertLineAfterAction extends EditorAction { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, EnterOperation.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 68614a2f432..9a4f6800938 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -42,6 +42,13 @@ export class MoveLinesCommand implements ICommand { public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + const getLanguageId = () => { + return model.getLanguageId(); + }; + const getLanguageIdAtPosition = (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }; + const modelLineCount = model.getLineCount(); if (this._isMovingDown && this._selection.endLineNumber === modelLineCount) { @@ -63,20 +70,6 @@ export class MoveLinesCommand implements ICommand { const { tabSize, indentSize, insertSpaces } = model.getOptions(); const indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces); - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - return model.tokenization.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); - }, - }, - getLineContent: null as unknown as (lineNumber: number) => string, - }; if (s.startLineNumber === s.endLineNumber && model.getLineMaxColumn(s.startLineNumber) === 1) { // Current line is empty @@ -120,12 +113,25 @@ export class MoveLinesCommand implements ICommand { insertingText = newIndentation + this.trimStart(movingLineText); } else { // no enter rule matches, let's check indentatin rules then. - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return model.getLineContent(movingLineNumber); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return model.tokenization.getLineTokens(movingLineNumber); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return model.getLineContent(movingLineNumber); + } else { + return model.getLineContent(lineNumber); + } + }, }; const indentOfMovingLine = getGoodIndentForLine( this._autoIndent, @@ -159,14 +165,30 @@ export class MoveLinesCommand implements ICommand { } } else { // it doesn't match onEnter rules, let's check indentation rules then. - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return insertingText; - } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { - return model.getLineContent(lineNumber - 1); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + // TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this. + return model.tokenization.getLineTokens(movingLineNumber); + } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { + return model.tokenization.getLineTokens(lineNumber - 1); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return insertingText; + } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { + return model.getLineContent(lineNumber - 1); + } else { + return model.getLineContent(lineNumber); + } + }, }; const newIndentatOfMovingBlock = getGoodIndentForLine( @@ -204,12 +226,25 @@ export class MoveLinesCommand implements ICommand { builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText); if (this.shouldAutoIndent(model, s)) { - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === movingLineNumber) { - return model.getLineContent(s.startLineNumber); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === movingLineNumber) { + return model.tokenization.getLineTokens(s.startLineNumber); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === movingLineNumber) { + return model.getLineContent(s.startLineNumber); + } else { + return model.getLineContent(lineNumber); + } + }, }; const ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2); diff --git a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts index c53496fb5de..48348ae9690 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; import { CopyLinesCommand } from 'vs/editor/contrib/linesOperations/browser/copyLinesCommand'; diff --git a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts index 5425697a2e4..f99fb62422d 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts index 750eb192c43..65941bd977b 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts @@ -14,39 +14,42 @@ import { MoveLinesCommand } from 'vs/editor/contrib/linesOperations/browser/move import { testCommand } from 'vs/editor/test/browser/testCommand'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +const enum MoveLinesDirection { + Up, + Down +} + function testMoveLinesDownCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService?: ILanguageConfigurationService): void { - const disposables = new DisposableStore(); - if (!languageConfigurationService) { - languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); - } - testCommand(lines, null, selection, (accessor, sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Advanced, languageConfigurationService), expectedLines, expectedSelection); - disposables.dispose(); + testMoveLinesUpOrDownCommand(MoveLinesDirection.Down, lines, selection, expectedLines, expectedSelection, languageConfigurationService); } function testMoveLinesUpCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService?: ILanguageConfigurationService): void { - const disposables = new DisposableStore(); - if (!languageConfigurationService) { - languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); - } - testCommand(lines, null, selection, (accessor, sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Advanced, languageConfigurationService), expectedLines, expectedSelection); - disposables.dispose(); + testMoveLinesUpOrDownCommand(MoveLinesDirection.Up, lines, selection, expectedLines, expectedSelection, languageConfigurationService); } function testMoveLinesDownWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService?: ILanguageConfigurationService): void { - const disposables = new DisposableStore(); - if (!languageConfigurationService) { - languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); - } - testCommand(lines, languageId, selection, (accessor, sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Full, languageConfigurationService), expectedLines, expectedSelection); - disposables.dispose(); + testMoveLinesUpOrDownWithIndentCommand(MoveLinesDirection.Down, languageId, lines, selection, expectedLines, expectedSelection, languageConfigurationService); } function testMoveLinesUpWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService?: ILanguageConfigurationService): void { + testMoveLinesUpOrDownWithIndentCommand(MoveLinesDirection.Up, languageId, lines, selection, expectedLines, expectedSelection, languageConfigurationService); +} + +function testMoveLinesUpOrDownCommand(direction: MoveLinesDirection, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService?: ILanguageConfigurationService) { const disposables = new DisposableStore(); if (!languageConfigurationService) { languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); } - testCommand(lines, languageId, selection, (accessor, sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Full, languageConfigurationService), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new MoveLinesCommand(sel, direction === MoveLinesDirection.Up ? false : true, EditorAutoIndentStrategy.Advanced, languageConfigurationService), expectedLines, expectedSelection); + disposables.dispose(); +} + +function testMoveLinesUpOrDownWithIndentCommand(direction: MoveLinesDirection, languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService?: ILanguageConfigurationService) { + const disposables = new DisposableStore(); + if (!languageConfigurationService) { + languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); + } + testCommand(lines, languageId, selection, (accessor, sel) => new MoveLinesCommand(sel, direction === MoveLinesDirection.Up ? false : true, EditorAutoIndentStrategy.Full, languageConfigurationService), expectedLines, expectedSelection); disposables.dispose(); } diff --git a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts index 982dc07426b..80d6ed93730 100644 --- a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts +++ b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts index 64de23fce27..008150e7c09 100644 --- a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css index 93758171dac..3efac6c122c 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css @@ -69,6 +69,10 @@ border-bottom: 1px solid var(--vscode-editorHoverWidget-border); } +.monaco-editor .parameter-hints-widget .code { + font-family: var(--vscode-parameterHintsWidget-editorFontFamily), var(--vscode-parameterHintsWidget-editorFontFamilyDefault); +} + .monaco-editor .parameter-hints-widget .docs { padding: 0 10px 0 5px; white-space: pre-wrap; diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts index 46d92a532e9..32546cff237 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts @@ -14,7 +14,7 @@ import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { assertIsDefined } from 'vs/base/common/types'; import 'vs/css!./parameterHints'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as languages from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -26,6 +26,8 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { listHighlightForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { ThemeIcon } from 'vs/base/common/themables'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const $ = dom.$; @@ -61,6 +63,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @ILanguageService languageService: ILanguageService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); @@ -123,9 +126,13 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { if (!this.domNodes) { return; } + const fontInfo = this.editor.getOption(EditorOption.fontInfo); - this.domNodes.element.style.fontSize = `${fontInfo.fontSize}px`; - this.domNodes.element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`; + const element = this.domNodes.element; + element.style.fontSize = `${fontInfo.fontSize}px`; + element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`; + element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamily', fontInfo.fontFamily); + element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily); }; updateFont(); @@ -200,10 +207,6 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } const code = dom.append(this.domNodes.signature, $('.code')); - const fontInfo = this.editor.getOption(EditorOption.fontInfo); - code.style.fontSize = `${fontInfo.fontSize}px`; - code.style.fontFamily = fontInfo.fontFamily; - const hasParameters = signature.parameters.length > 0; const activeParameterIndex = signature.activeParameter ?? hints.activeParameter; @@ -272,12 +275,30 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } private renderMarkdownDocs(markdown: IMarkdownString | undefined): IMarkdownRenderResult { + const stopWatch = new StopWatch(); const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(markdown, { asyncRenderCallback: () => { this.domNodes?.scrollbar.scanDomNode(); } })); renderedContents.element.classList.add('markdown-docs'); + + type RenderMarkdownPerformanceClassification = { + owner: 'donjayamanne'; + comment: 'Measure the time taken to render markdown for parameter hints'; + renderDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Time in ms to render the markdown' }; + }; + + type RenderMarkdownPerformanceEvent = { + renderDuration: number; + }; + const renderDuration = stopWatch.elapsed(); + if (renderDuration > 300) { + this.telemetryService.publicLog2('parameterHints.parseMarkdown', { + renderDuration + }); + } + return renderedContents; } @@ -366,4 +387,4 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } } -registerColor('editorHoverWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.')); +registerColor('editorHoverWidget.highlightForeground', listHighlightForeground, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.')); diff --git a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts index 6c84215bc72..10372b408c4 100644 --- a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/peekView/browser/peekView.ts b/src/vs/editor/contrib/peekView/browser/peekView.ts index a0f2dfd914e..86f6cb1d47f 100644 --- a/src/vs/editor/contrib/peekView/browser/peekView.ts +++ b/src/vs/editor/contrib/peekView/browser/peekView.ts @@ -289,8 +289,8 @@ export const peekViewResultsFileForeground = registerColor('peekViewResult.fileF export const peekViewResultsSelectionBackground = registerColor('peekViewResult.selectionBackground', { dark: '#3399ff33', light: '#3399ff33', hcDark: null, hcLight: null }, nls.localize('peekViewResultsSelectionBackground', 'Background color of the selected entry in the peek view result list.')); export const peekViewResultsSelectionForeground = registerColor('peekViewResult.selectionForeground', { dark: Color.white, light: '#6C6C6C', hcDark: Color.white, hcLight: editorForeground }, nls.localize('peekViewResultsSelectionForeground', 'Foreground color of the selected entry in the peek view result list.')); export const peekViewEditorBackground = registerColor('peekViewEditor.background', { dark: '#001F33', light: '#F2F8FC', hcDark: Color.black, hcLight: Color.white }, nls.localize('peekViewEditorBackground', 'Background color of the peek view editor.')); -export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutter.background', { dark: peekViewEditorBackground, light: peekViewEditorBackground, hcDark: peekViewEditorBackground, hcLight: peekViewEditorBackground }, nls.localize('peekViewEditorGutterBackground', 'Background color of the gutter in the peek view editor.')); -export const peekViewEditorStickyScrollBackground = registerColor('peekViewEditorStickyScroll.background', { dark: peekViewEditorBackground, light: peekViewEditorBackground, hcDark: peekViewEditorBackground, hcLight: peekViewEditorBackground }, nls.localize('peekViewEditorStickScrollBackground', 'Background color of sticky scroll in the peek view editor.')); +export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutter.background', peekViewEditorBackground, nls.localize('peekViewEditorGutterBackground', 'Background color of the gutter in the peek view editor.')); +export const peekViewEditorStickyScrollBackground = registerColor('peekViewEditorStickyScroll.background', peekViewEditorBackground, nls.localize('peekViewEditorStickScrollBackground', 'Background color of sticky scroll in the peek view editor.')); export const peekViewResultsMatchHighlight = registerColor('peekViewResult.matchHighlightBackground', { dark: '#ea5c004d', light: '#ea5c004d', hcDark: null, hcLight: null }, nls.localize('peekViewResultsMatchHighlight', 'Match highlight color in the peek view result list.')); export const peekViewEditorMatchHighlight = registerColor('peekViewEditor.matchHighlightBackground', { dark: '#ff8f0099', light: '#f5d802de', hcDark: null, hcLight: null }, nls.localize('peekViewEditorMatchHighlight', 'Match highlight color in the peek view editor.')); diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts new file mode 100644 index 00000000000..e9994c4b01c --- /dev/null +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.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 'vs/css!./placeholderText'; +import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ghostTextForeground } from 'vs/editor/common/core/editorColorRegistry'; +import { localize } from 'vs/nls'; +import { registerColor } from 'vs/platform/theme/common/colorUtils'; +import { PlaceholderTextContribution } from './placeholderTextContribution'; +import { wrapInReloadableClass1 } from 'vs/platform/observable/common/wrapInReloadableClass'; + +registerEditorContribution(PlaceholderTextContribution.ID, wrapInReloadableClass1(() => PlaceholderTextContribution), EditorContributionInstantiation.Eager); + +registerColor('editor.placeholder.foreground', ghostTextForeground, localize('placeholderForeground', 'Foreground color of the placeholder text in the editor.')); diff --git a/build/azure-pipelines/common/installPlaywright.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderText.css similarity index 53% rename from build/azure-pipelines/common/installPlaywright.ts rename to src/vs/editor/contrib/placeholderText/browser/placeholderText.css index 742b6e0e399..043b6f15632 100644 --- a/build/azure-pipelines/common/installPlaywright.ts +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderText.css @@ -3,12 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -process.env.DEBUG='pw:install'; // enable logging for this (https://github.com/microsoft/playwright/issues/17394) +.monaco-editor { + --vscode-editor-placeholder-foreground: var(--vscode-editorGhostText-foreground); -const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/server'); + .editorPlaceholder { + top: 0px; + position: absolute; + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; + pointer-events: none; -async function install() { - await installDefaultBrowsersForNpmInstall(); + color: var(--vscode-editor-placeholder-foreground); + } } - -install(); diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts new file mode 100644 index 00000000000..f048991d0f9 --- /dev/null +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/browser/dom'; +import { structuralEquals } from 'vs/base/common/equals'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { autorun, constObservable, derivedObservableWithCache, derivedOpts, IObservable, IReader } from 'vs/base/common/observable'; +import { DebugOwner } from 'vs/base/common/observableInternal/debugName'; +import { derivedWithStore } from 'vs/base/common/observableInternal/derived'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; + +/** + * Use the editor option to set the placeholder text. +*/ +export class PlaceholderTextContribution extends Disposable implements IEditorContribution { + public static get(editor: ICodeEditor): PlaceholderTextContribution { + return editor.getContribution(PlaceholderTextContribution.ID)!; + } + + public static readonly ID = 'editor.contrib.placeholderText'; + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _placeholderText = this._editorObs.getOption(EditorOption.placeholder); + + private readonly _state = derivedOpts<{ placeholder: string } | undefined>({ owner: this, equalsFn: structuralEquals }, reader => { + const p = this._placeholderText.read(reader); + if (!p) { return undefined; } + if (!this._editorObs.valueIsEmpty.read(reader)) { return undefined; } + return { placeholder: p }; + }); + + private readonly _shouldViewBeAlive = isOrWasTrue(this, reader => this._state.read(reader)?.placeholder !== undefined); + + private readonly _view = derivedWithStore((reader, store) => { + if (!this._shouldViewBeAlive.read(reader)) { return; } + + const element = h('div.editorPlaceholder'); + + store.add(autorun(reader => { + const data = this._state.read(reader); + const shouldBeVisibile = data?.placeholder !== undefined; + element.root.style.display = shouldBeVisibile ? 'block' : 'none'; + element.root.innerText = data?.placeholder ?? ''; + })); + store.add(autorun(reader => { + const info = this._editorObs.layoutInfo.read(reader); + element.root.style.left = `${info.contentLeft}px`; + element.root.style.width = (info.contentWidth - info.verticalScrollbarWidth) + 'px'; + element.root.style.top = `${this._editor.getTopForLineNumber(0)}px`; + })); + store.add(autorun(reader => { + element.root.style.fontFamily = this._editorObs.getOption(EditorOption.fontFamily).read(reader); + element.root.style.fontSize = this._editorObs.getOption(EditorOption.fontSize).read(reader) + 'px'; + element.root.style.lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader) + 'px'; + })); + store.add(this._editorObs.createOverlayWidget({ + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + position: constObservable(null), + domNode: element.root, + })); + }); + + constructor( + private readonly _editor: ICodeEditor, + ) { + super(); + this._view.recomputeInitiallyAndOnChange(this._store); + } +} + +function isOrWasTrue(owner: DebugOwner, fn: (reader: IReader) => boolean): IObservable { + return derivedObservableWithCache(owner, (reader, lastValue) => { + if (lastValue === true) { return true; } + return fn(reader); + }); +} diff --git a/src/vs/editor/contrib/quickAccess/browser/commandsQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/commandsQuickAccess.ts index 9b91b082ab6..9334d7fcde1 100644 --- a/src/vs/editor/contrib/quickAccess/browser/commandsQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/commandsQuickAccess.ts @@ -5,6 +5,8 @@ import { stripIcons } from 'vs/base/common/iconLabels'; import { IEditor } from 'vs/editor/common/editorCommon'; +import { ILocalizedString } from 'vs/nls'; +import { isLocalizedString } from 'vs/platform/action/common/action'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -38,9 +40,18 @@ export abstract class AbstractEditorCommandsQuickAccessProvider extends Abstract const editorCommandPicks: ICommandQuickPick[] = []; for (const editorAction of activeTextEditorControl.getSupportedActions()) { + let commandDescription: undefined | ILocalizedString; + if (editorAction.metadata?.description) { + if (isLocalizedString(editorAction.metadata.description)) { + commandDescription = editorAction.metadata.description; + } else { + commandDescription = { original: editorAction.metadata.description, value: editorAction.metadata.description }; + } + } editorCommandPicks.push({ commandId: editorAction.id, commandAlias: editorAction.alias, + commandDescription, label: stripIcons(editorAction.label) || editorAction.id, }); } diff --git a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts index de4198e7446..c809937fa72 100644 --- a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts @@ -12,7 +12,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { IDiffEditor, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, ITextModel, OverviewRulerLane } from 'vs/editor/common/model'; import { overviewRulerRangeHighlight } from 'vs/editor/common/core/editorColorRegistry'; -import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IKeyMods, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { status } from 'vs/base/browser/ui/aria/aria'; @@ -52,7 +52,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu //#region Provider methods - provide(picker: IQuickPick, token: CancellationToken): IDisposable { + provide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // Apply options if any @@ -63,7 +63,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu // Provide based on current active editor const pickerDisposable = disposables.add(new MutableDisposable()); - pickerDisposable.value = this.doProvide(picker, token); + pickerDisposable.value = this.doProvide(picker, token, runOptions); // Re-create whenever the active editor changes disposables.add(this.onDidActiveTextEditorControlChange(() => { @@ -78,7 +78,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu return disposables; } - private doProvide(picker: IQuickPick, token: CancellationToken): IDisposable { + private doProvide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // With text control @@ -113,7 +113,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu disposables.add(toDisposable(() => this.clearDecorations(editor))); // Ask subclass for entries - disposables.add(this.provideWithTextEditor(context, picker, token)); + disposables.add(this.provideWithTextEditor(context, picker, token, runOptions)); } // Without text control @@ -134,12 +134,12 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu /** * Subclasses to implement to provide picks for the picker when an editor is active. */ - protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable; + protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable; /** * Subclasses to implement to provide picks for the picker when no editor is active. */ - protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; + protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; protected gotoLocation({ editor }: IQuickAccessTextEditorContext, options: { range: IRange; keyMods: IKeyMods; forceSideBySide?: boolean; preserveFocus?: boolean }): void { editor.setSelection(options.range, TextEditorSelectionSource.JUMP); diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts index 6db58ae6cc0..b1ccfa95d51 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts @@ -24,7 +24,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor super({ canAcceptInBackground: true }); } - protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line."); picker.items = [{ label }]; @@ -33,7 +33,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor return Disposable.None; } - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { + protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { const editor = context.editor; const disposables = new DisposableStore(); diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts index 01092241b7a..9ff33f41802 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts @@ -22,23 +22,33 @@ import { IQuickInputButton, IQuickPick, IQuickPickItem, IQuickPickSeparator } fr import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { Position } from 'vs/editor/common/core/position'; import { findLast } from 'vs/base/common/arraysFind'; +import { IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; +import { URI } from 'vs/base/common/uri'; export interface IGotoSymbolQuickPickItem extends IQuickPickItem { kind: SymbolKind; index: number; score?: number; + uri?: URI; + symbolName?: string; range?: { decoration: IRange; selection: IRange }; } export interface IGotoSymbolQuickAccessProviderOptions extends IEditorNavigationQuickAccessOptions { openSideBySideDirection?: () => undefined | 'right' | 'down'; + /** + * A handler to invoke when an item is accepted for + * this particular showing of the quick access. + * @param item The item that was accepted. + */ + readonly handleAccept?: (item: IQuickPickItem) => void; } export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider { static PREFIX = '@'; static SCOPE_PREFIX = ':'; - static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`; + static PREFIX_BY_CATEGORY = `${this.PREFIX}${this.SCOPE_PREFIX}`; protected override readonly options: IGotoSymbolQuickAccessProviderOptions; @@ -53,13 +63,13 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit this.options.canAcceptInBackground = true; } - protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { this.provideLabelPick(picker, localize('cannotRunGotoSymbolWithoutEditor', "To go to a symbol, first open a text editor with symbol information.")); return Disposable.None; } - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { + protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const editor = context.editor; const model = this.getModel(editor); if (!model) { @@ -68,7 +78,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Provide symbols from model if available in registry if (this._languageFeaturesService.documentSymbolProvider.has(model)) { - return this.doProvideWithEditorSymbols(context, model, picker, token); + return this.doProvideWithEditorSymbols(context, model, picker, token, runOptions); } // Otherwise show an entry for a model without registry @@ -77,7 +87,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return this.doProvideWithoutEditorSymbols(context, model, picker, token); } - private doProvideWithoutEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { + private doProvideWithoutEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { const disposables = new DisposableStore(); // Generic pick for not having any symbol information @@ -100,7 +110,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return disposables; } - private provideLabelPick(picker: IQuickPick, label: string): void { + private provideLabelPick(picker: IQuickPick, label: string): void { picker.items = [{ label, index: 0, kind: SymbolKind.String }]; picker.ariaLabel = label; } @@ -127,7 +137,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return symbolProviderRegistryPromise.p; } - private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { + private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const editor = context.editor; const disposables = new DisposableStore(); @@ -137,6 +147,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit if (item && item.range) { this.gotoLocation(context, { range: item.range.selection, keyMods: picker.keyMods, preserveFocus: event.inBackground }); + runOptions?.handleAccept?.(item); + if (!event.inBackground) { picker.hide(); } @@ -171,7 +183,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit picker.busy = true; try { const query = prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim()); - const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token); + const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token, model); if (token.isCancellationRequested) { return; } @@ -218,7 +230,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return disposables; } - protected async doGetSymbolPicks(symbolsPromise: Promise, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken): Promise> { + protected async doGetSymbolPicks(symbolsPromise: Promise, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken, model: ITextModel): Promise> { const symbols = await symbolsPromise; if (token.isCancellationRequested) { return []; @@ -326,6 +338,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit selection: Range.collapseToStart(symbol.selectionRange), decoration: symbol.range }, + uri: model.uri, + symbolName: symbolLabel, strikethrough: deprecated, buttons }); diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index b112dd1103c..ef5783aa500 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -21,7 +21,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; -import { Rejection, RenameLocation, RenameProvider, WorkspaceEdit } from 'vs/editor/common/languages'; +import { NewSymbolNameTriggerKind, Rejection, RenameLocation, RenameProvider, WorkspaceEdit } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; @@ -231,7 +231,17 @@ class RenameController implements IEditorContribution { const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model); - const requestRenameSuggestions = (cts: CancellationToken) => newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, cts)); + const resolvedNewSymbolnamesProviders = await Promise.all(newSymbolNamesProviders.map(async p => [p, await p.supportsAutomaticNewSymbolNamesTriggerKind ?? false] as const)); + + const requestRenameSuggestions = (triggerKind: NewSymbolNameTriggerKind, cts: CancellationToken) => { + let providers = resolvedNewSymbolnamesProviders.slice(); + + if (triggerKind === NewSymbolNameTriggerKind.Automatic) { + providers = providers.filter(([_, supportsAutomatic]) => supportsAutomatic); + } + + return providers.map(([p,]) => p.provideNewSymbolNames(model, loc.range, triggerKind, cts)); + }; trace('creating rename input field and awaiting its result'); const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); @@ -239,7 +249,7 @@ class RenameController implements IEditorContribution { loc.range, loc.text, supportPreview, - requestRenameSuggestions, + newSymbolNamesProviders.length > 0 ? requestRenameSuggestions : undefined, cts2 ); trace('received response from rename input field'); @@ -349,6 +359,10 @@ class RenameController implements IEditorContribution { timeBeforeFirstInputFieldEdit?: number; /** provided only if kind = 'accepted' */ wantsPreview?: boolean; + /** provided only if kind = 'accepted' */ + nRenameSuggestionsInvocations?: number; + /** provided only if kind = 'accepted' */ + hadAutomaticRenameSuggestionsInvocation?: boolean; }; type RenameInvokedClassification = { @@ -357,12 +371,14 @@ class RenameController implements IEditorContribution { kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the rename operation was cancelled or accepted.' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Document language ID.' }; - nRenameSuggestionProviders: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of rename providers for this document.'; isMeasurement: true }; + nRenameSuggestionProviders: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of rename providers for this document.' }; source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the new name came from the input field or rename suggestions.' }; - nRenameSuggestions?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of rename suggestions user has got'; isMeasurement: true }; - timeBeforeFirstInputFieldEdit?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Milliseconds before user edits the input field for the first time'; isMeasurement: true }; - wantsPreview?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If user wanted preview.'; isMeasurement: true }; + nRenameSuggestions?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of rename suggestions user has got' }; + timeBeforeFirstInputFieldEdit?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Milliseconds before user edits the input field for the first time' }; + wantsPreview?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If user wanted preview.' }; + nRenameSuggestionsInvocations?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of times rename suggestions were invoked' }; + hadAutomaticRenameSuggestionsInvocation?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether rename suggestions were invoked automatically' }; }; const value: RenameInvokedEvent = @@ -381,6 +397,8 @@ class RenameController implements IEditorContribution { nRenameSuggestions: inputFieldResult.stats.nRenameSuggestions, timeBeforeFirstInputFieldEdit: inputFieldResult.stats.timeBeforeFirstInputFieldEdit, wantsPreview: inputFieldResult.wantsPreview, + nRenameSuggestionsInvocations: inputFieldResult.stats.nRenameSuggestionsInvocations, + hadAutomaticRenameSuggestionsInvocation: inputFieldResult.stats.hadAutomaticRenameSuggestionsInvocation, }; this._telemetryService.publicLog2('renameInvokedEvent', value); @@ -492,8 +510,7 @@ registerAction2(class FocusNextRenameSuggestion extends Action2 { precondition: CONTEXT_RENAME_INPUT_VISIBLE, keybinding: [ { - primary: KeyCode.Tab, - secondary: [KeyCode.DownArrow], + primary: KeyCode.DownArrow, weight: KeybindingWeight.EditorContrib + 99, } ] @@ -521,8 +538,7 @@ registerAction2(class FocusPreviousRenameSuggestion extends Action2 { precondition: CONTEXT_RENAME_INPUT_VISIBLE, keybinding: [ { - primary: KeyMod.Shift | KeyCode.Tab, - secondary: [KeyCode.UpArrow], + primary: KeyCode.UpArrow, weight: KeybindingWeight.EditorContrib + 99, } ] diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.css b/src/vs/editor/contrib/rename/browser/renameWidget.css index 9fa6ac1d3f9..66f241efd1c 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.css +++ b/src/vs/editor/contrib/rename/browser/renameWidget.css @@ -13,12 +13,39 @@ padding: 4px 4px 0 4px; } -.monaco-editor .rename-box .rename-input { +.monaco-editor .rename-box .rename-input-with-button { padding: 3px; border-radius: 2px; width: calc(100% - 8px); /* 4px padding on each side */ } +.monaco-editor .rename-box .rename-input { + width: calc(100% - 8px); /* 4px padding on each side */ + padding: 0; +} + +.monaco-editor .rename-box .rename-input:focus { + outline: none; +} + +.monaco-editor .rename-box .rename-suggestions-button { + display: flex; + align-items: center; + padding: 3px; + background-color: transparent; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.monaco-editor .rename-box .rename-suggestions-button:hover { + background-color: var(--vscode-toolbar-hoverBackground) +} + +.monaco-editor .rename-box .rename-candidate-list-container .monaco-list-row { + border-radius: 2px; +} + .monaco-editor .rename-box .rename-label { display: none; opacity: .8; diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index bf475fde028..b284cd519fd 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -4,7 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; +import { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; @@ -13,11 +17,12 @@ import { DeferredPromise, raceCancellation } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType, isDefined } from 'vs/base/common/types'; import 'vs/css!./renameWidget'; -import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; +import * as domFontInfo from 'vs/editor/browser/config/domFontInfo'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; @@ -25,8 +30,8 @@ import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { NewSymbolName, NewSymbolNameTag, ProviderResult } from 'vs/editor/common/languages'; -import { localize } from 'vs/nls'; +import { NewSymbolName, NewSymbolNameTag, NewSymbolNameTriggerKind, ProviderResult } from 'vs/editor/common/languages'; +import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; @@ -49,8 +54,8 @@ const _sticky = false ; -export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false, localize('renameInputVisible', "Whether the rename input widget is visible")); -export const CONTEXT_RENAME_INPUT_FOCUSED = new RawContextKey('renameInputFocused', false, localize('renameInputFocused', "Whether the rename input widget is focused")); +export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false, nls.localize('renameInputVisible', "Whether the rename input widget is visible")); +export const CONTEXT_RENAME_INPUT_FOCUSED = new RawContextKey('renameInputFocused', false, nls.localize('renameInputFocused', "Whether the rename input widget is focused")); /** * "Source" of the new name: @@ -70,6 +75,8 @@ export type RenameWidgetStats = { nRenameSuggestions: number; source: NewNameSource; timeBeforeFirstInputFieldEdit: number | undefined; + nRenameSuggestionsInvocations: number; + hadAutomaticRenameSuggestionsInvocation: boolean; }; export type RenameWidgetResult = { @@ -89,7 +96,7 @@ interface IRenameWidget { where: IRange, currentName: string, supportPreview: boolean, - requestRenameSuggestions: (cts: CancellationToken) => ProviderResult[], + requestRenameSuggestions: (triggerKind: NewSymbolNameTriggerKind, cts: CancellationToken) => ProviderResult[], cts: CancellationTokenSource ): Promise; @@ -108,7 +115,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable // UI state private _domNode?: HTMLElement; - private _input: RenameInput; + private _inputWithButton: InputWithButton; private _renameCandidateListView?: RenameCandidateListView; private _label?: HTMLDivElement; @@ -122,6 +129,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable /** Is true if input field got changes when a rename candidate was focused; otherwise, false */ private _isEditingRenameCandidate: boolean; + private readonly _candidates: Set; + private _visible?: boolean; /** must be reset at session start */ @@ -133,7 +142,12 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable */ private _timeBeforeFirstInputFieldEdit: number | undefined; + private _nRenameSuggestionsInvocations: number; + + private _hadAutomaticRenameSuggestionsInvocation: boolean; + private _renameCandidateProvidersCts: CancellationTokenSource | undefined; + private _renameCts: CancellationTokenSource | undefined; private readonly _visibleContextKey: IContextKey; private readonly _disposables = new DisposableStore(); @@ -150,10 +164,16 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable this._isEditingRenameCandidate = false; + this._nRenameSuggestionsInvocations = 0; + + this._hadAutomaticRenameSuggestionsInvocation = false; + + this._candidates = new Set(); + this._beforeFirstInputFieldEditSW = new StopWatch(); - this._input = new RenameInput(); - this._disposables.add(this._input); + this._inputWithButton = new InputWithButton(); + this._disposables.add(this._inputWithButton); this._editor.addContentWidget(this); @@ -180,13 +200,13 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable this._domNode = document.createElement('div'); this._domNode.className = 'monaco-editor rename-box'; - this._domNode.appendChild(this._input.domNode); + this._domNode.appendChild(this._inputWithButton.domNode); this._renameCandidateListView = this._disposables.add( new RenameCandidateListView(this._domNode, { fontInfo: this._editor.getOption(EditorOption.fontInfo), onFocusChange: (newSymbolName: string) => { - this._input.domNode.value = newSymbolName; + this._inputWithButton.input.value = newSymbolName; this._isEditingRenameCandidate = false; // @ulugbekna: reset }, onSelectionChange: () => { @@ -197,7 +217,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable ); this._disposables.add( - this._input.onDidChange(() => { + this._inputWithButton.onDidInputChange(() => { if (this._renameCandidateListView?.focusedCandidate !== undefined) { this._isEditingRenameCandidate = true; } @@ -231,12 +251,13 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable this._domNode.style.border = widgetBorderColor ? `1px solid ${widgetBorderColor}` : ''; this._domNode.style.color = String(theme.getColor(inputForeground) ?? ''); - this._input.domNode.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); - // this._input.style.color = String(theme.getColor(inputForeground) ?? ''); const border = theme.getColor(inputBorder); - this._input.domNode.style.borderWidth = border ? '1px' : '0px'; - this._input.domNode.style.borderStyle = border ? 'solid' : 'none'; - this._input.domNode.style.borderColor = border?.toString() ?? 'none'; + + this._inputWithButton.domNode.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); + this._inputWithButton.input.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); + this._inputWithButton.domNode.style.borderWidth = border ? '1px' : '0px'; + this._inputWithButton.domNode.style.borderStyle = border ? 'solid' : 'none'; + this._inputWithButton.domNode.style.borderColor = border?.toString() ?? 'none'; } private _updateFont(): void { @@ -245,7 +266,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable } assertType(this._label !== undefined, 'RenameWidget#_updateFont: _label must not be undefined given _domNode is defined'); - this._editor.applyFontInfo(this._input.domNode); + this._editor.applyFontInfo(this._inputWithButton.input); const fontInfo = this._editor.getOption(EditorOption.fontInfo); this._label.style.fontSize = `${this._computeLabelFontSize(fontInfo.fontSize)}px`; @@ -289,7 +310,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable beforeRender(): IDimension | null { const [accept, preview] = this._acceptKeybindings; - this._label!.innerText = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Rename, Shift+F2 to Preview"'] }, "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); + this._label!.innerText = nls.localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Rename, Shift+F2 to Preview"'] }, "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); this._domNode!.style.minWidth = `200px`; // to prevent from widening when candidates come in @@ -314,7 +335,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable assertType(this._nPxAvailableAbove !== undefined); assertType(this._nPxAvailableBelow !== undefined); - const inputBoxHeight = dom.getTotalHeight(this._input.domNode); + const inputBoxHeight = dom.getTotalHeight(this._inputWithButton.domNode); const labelHeight = dom.getTotalHeight(this._label!); @@ -327,13 +348,14 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable this._renameCandidateListView!.layout({ height: totalHeightAvailable - labelHeight - inputBoxHeight, - width: dom.getTotalWidth(this._input.domNode), + width: dom.getTotalWidth(this._inputWithButton.domNode), }); } private _currentAcceptInput?: (wantsPreview: boolean) => void; private _currentCancelInput?: (focusEditor: boolean) => void; + private _requestRenameCandidatesOnce?: (triggerKind: NewSymbolNameTriggerKind, cts: CancellationToken) => ProviderResult[]; acceptInput(wantsPreview: boolean): void { this._trace(`invoking acceptInput`); @@ -347,29 +369,65 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable focusNextRenameSuggestion() { if (!this._renameCandidateListView?.focusNext()) { - this._input.domNode.value = this._currentName!; + this._inputWithButton.input.value = this._currentName!; } } focusPreviousRenameSuggestion() { // TODO@ulugbekna: this and focusNext should set the original name if no candidate is focused if (!this._renameCandidateListView?.focusPrevious()) { - this._input.domNode.value = this._currentName!; + this._inputWithButton.input.value = this._currentName!; } } + /** + * @param requestRenameCandidates is `undefined` when there are no rename suggestion providers + */ getInput( where: IRange, currentName: string, supportPreview: boolean, - requestRenameCandidates: (cts: CancellationToken) => ProviderResult[], + requestRenameCandidates: undefined | ((triggerKind: NewSymbolNameTriggerKind, cts: CancellationToken) => ProviderResult[]), cts: CancellationTokenSource ): Promise { const { start: selectionStart, end: selectionEnd } = this._getSelection(where, currentName); - this._renameCandidateProvidersCts = new CancellationTokenSource(); - const candidates = requestRenameCandidates(this._renameCandidateProvidersCts.token); - this._updateRenameCandidates(candidates, currentName, cts.token); + this._renameCts = cts; + + const disposeOnDone = new DisposableStore(); + + this._nRenameSuggestionsInvocations = 0; + + this._hadAutomaticRenameSuggestionsInvocation = false; + + if (requestRenameCandidates === undefined) { + this._inputWithButton.button.style.display = 'none'; + } else { + this._inputWithButton.button.style.display = 'flex'; + + this._requestRenameCandidatesOnce = requestRenameCandidates; + + this._requestRenameCandidates(currentName, false); + + disposeOnDone.add(dom.addDisposableListener( + this._inputWithButton.button, + 'click', + () => this._requestRenameCandidates(currentName, true) + )); + disposeOnDone.add(dom.addDisposableListener( + this._inputWithButton.button, + dom.EventType.KEY_DOWN, + (e) => { + const keyEvent = new StandardKeyboardEvent(e); + + if (keyEvent.equals(KeyCode.Enter) || keyEvent.equals(KeyCode.Space)) { + keyEvent.stopPropagation(); + keyEvent.preventDefault(); + this._requestRenameCandidates(currentName, true); + } + } + )); + } this._isEditingRenameCandidate = false; @@ -378,16 +436,18 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable this._position = new Position(where.startLineNumber, where.startColumn); this._currentName = currentName; - this._input.domNode.value = currentName; - this._input.domNode.setAttribute('selectionStart', selectionStart.toString()); - this._input.domNode.setAttribute('selectionEnd', selectionEnd.toString()); - this._input.domNode.size = Math.max((where.endColumn - where.startColumn) * 1.1, 20); // determines width + this._inputWithButton.input.value = currentName; + this._inputWithButton.input.setAttribute('selectionStart', selectionStart.toString()); + this._inputWithButton.input.setAttribute('selectionEnd', selectionEnd.toString()); + this._inputWithButton.input.size = Math.max((where.endColumn - where.startColumn) * 1.1, 20); // determines width this._beforeFirstInputFieldEditSW.reset(); - const disposeOnDone = new DisposableStore(); - disposeOnDone.add(toDisposable(() => cts.dispose(true))); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call + disposeOnDone.add(toDisposable(() => { + this._renameCts = undefined; + cts.dispose(true); + })); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call disposeOnDone.add(toDisposable(() => { if (this._renameCandidateProvidersCts !== undefined) { this._renameCandidateProvidersCts.dispose(true); @@ -395,6 +455,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable } })); + disposeOnDone.add(toDisposable(() => this._candidates.clear())); + const inputResult = new DeferredPromise(); inputResult.p.finally(() => { @@ -406,6 +468,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable this._trace('invoking _currentCancelInput'); this._currentAcceptInput = undefined; this._currentCancelInput = undefined; + // fixme session cleanup this._renameCandidateListView?.clearCandidates(); inputResult.complete(focusEditor); return true; @@ -426,7 +489,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable source = { k: 'renameSuggestion' }; } else { this._trace('using new name from inputField'); - newName = this._input.domNode.value; + newName = this._inputWithButton.input.value; source = this._isEditingRenameCandidate ? { k: 'userEditedRenameSuggestion' } : { k: 'inputField' }; } @@ -438,6 +501,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable this._currentAcceptInput = undefined; this._currentCancelInput = undefined; this._renameCandidateListView.clearCandidates(); + // fixme session cleanup inputResult.complete({ newName, @@ -446,6 +510,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable source, nRenameSuggestions, timeBeforeFirstInputFieldEdit: this._timeBeforeFirstInputFieldEdit, + nRenameSuggestionsInvocations: this._nRenameSuggestionsInvocations, + hadAutomaticRenameSuggestionsInvocation: this._hadAutomaticRenameSuggestionsInvocation, } }); }; @@ -460,6 +526,40 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable return inputResult.p; } + private _requestRenameCandidates(currentName: string, isManuallyTriggered: boolean) { + if (this._requestRenameCandidatesOnce === undefined) { + return; + } + if (this._renameCandidateProvidersCts !== undefined) { + this._renameCandidateProvidersCts.dispose(true); + } + + assertType(this._renameCts); + + if (this._inputWithButton.buttonState !== 'stop') { + + this._renameCandidateProvidersCts = new CancellationTokenSource(); + + const triggerKind = isManuallyTriggered ? NewSymbolNameTriggerKind.Invoke : NewSymbolNameTriggerKind.Automatic; + const candidates = this._requestRenameCandidatesOnce(triggerKind, this._renameCandidateProvidersCts.token); + + if (candidates.length === 0) { + this._inputWithButton.setSparkleButton(); + return; + } + + if (!isManuallyTriggered) { + this._hadAutomaticRenameSuggestionsInvocation = true; + } + + this._nRenameSuggestionsInvocations += 1; + + this._inputWithButton.setStopButton(); + + this._updateRenameCandidates(candidates, currentName, this._renameCts.token); + } + } + /** * This allows selecting only part of the symbol name in the input field based on the selection in the editor */ @@ -487,10 +587,10 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable // TODO@ulugbekna: could this be simply run in `afterRender`? setTimeout(() => { - this._input.domNode.focus(); - this._input.domNode.setSelectionRange( - parseInt(this._input!.domNode.getAttribute('selectionStart')!), - parseInt(this._input!.domNode.getAttribute('selectionEnd')!) + this._inputWithButton.input.focus(); + this._inputWithButton.input.setSelectionRange( + parseInt(this._inputWithButton.input.getAttribute('selectionStart')!), + parseInt(this._inputWithButton.input.getAttribute('selectionEnd')!) ); }, 100); } @@ -501,6 +601,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable trace('start'); const namesListResults = await raceCancellation(Promise.allSettled(candidates), token); + this._inputWithButton.setSparkleButton(); + if (namesListResults === undefined) { trace('returning early - received updateRenameCandidates results - undefined'); return; @@ -514,12 +616,15 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable trace(`received updateRenameCandidates results - total (unfiltered) ${newNames.length} candidates.`); // deduplicate and filter out the current value + const distinctNames = arrays.distinct(newNames, v => v.newSymbolName); trace(`distinct candidates - ${distinctNames.length} candidates.`); - const validDistinctNames = distinctNames.filter(({ newSymbolName }) => newSymbolName.trim().length > 0 && newSymbolName !== this._input.domNode.value && newSymbolName !== currentName); + const validDistinctNames = distinctNames.filter(({ newSymbolName }) => newSymbolName.trim().length > 0 && newSymbolName !== this._inputWithButton.input.value && newSymbolName !== currentName && !this._candidates.has(newSymbolName)); trace(`valid distinct candidates - ${newNames.length} candidates.`); + validDistinctNames.forEach(n => this._candidates.add(n.newSymbolName)); + if (validDistinctNames.length < 1) { trace('returning early - no valid distinct candidates'); return; @@ -569,7 +674,7 @@ class RenameCandidateListView { private _minimumWidth: number; private _typicalHalfwidthCharacterWidth: number; - private _disposables: DisposableStore; + private readonly _disposables: DisposableStore; // FIXME@ulugbekna: rewrite using event emitters constructor(parent: HTMLElement, opts: { fontInfo: FontInfo; onFocusChange: (newSymbolName: string) => void; onSelectionChange: () => void }) { @@ -583,6 +688,7 @@ class RenameCandidateListView { this._typicalHalfwidthCharacterWidth = opts.fontInfo.typicalHalfwidthCharacterWidth; this._listContainer = document.createElement('div'); + this._listContainer.className = 'rename-box rename-candidate-list-container'; parent.appendChild(this._listContainer); this._listWidget = RenameCandidateListView._createListWidget(this._listContainer, this._candidateViewHeight, opts.fontInfo); @@ -634,7 +740,7 @@ class RenameCandidateListView { this._listWidget.splice(0, 0, candidates); // adjust list widget layout - const height = this._pickListHeight(candidates.length); + const height = this._pickListHeight(this._listWidget.length); const width = this._pickListWidth(candidates); this._listWidget.layout(height, width); @@ -643,7 +749,7 @@ class RenameCandidateListView { this._listContainer.style.height = `${height}px`; this._listContainer.style.width = `${width}px`; - aria.status(localize('renameSuggestionsReceivedAria', "Received {0} rename suggestions", candidates.length)); + aria.status(nls.localize('renameSuggestionsReceivedAria', "Received {0} rename suggestions", candidates.length)); } public clearCandidates(): void { @@ -678,13 +784,17 @@ class RenameCandidateListView { const focusedIxs = this._listWidget.getFocus(); if (focusedIxs.length === 0) { this._listWidget.focusFirst(); + this._listWidget.reveal(0); return true; } else { if (focusedIxs[0] === this._listWidget.length - 1) { this._listWidget.setFocus([]); + this._listWidget.reveal(0); // @ulugbekna: without this, it seems like focused element is obstructed return false; } else { this._listWidget.focusNext(); + const focused = this._listWidget.getFocus()[0]; + this._listWidget.reveal(focused); return true; } } @@ -700,6 +810,8 @@ class RenameCandidateListView { const focusedIxs = this._listWidget.getFocus(); if (focusedIxs.length === 0) { this._listWidget.focusLast(); + const focused = this._listWidget.getFocus()[0]; + this._listWidget.reveal(focused); return true; } else { if (focusedIxs[0] === 0) { @@ -707,6 +819,8 @@ class RenameCandidateListView { return false; } else { this._listWidget.focusPrevious(); + const focused = this._listWidget.getFocus()[0]; + this._listWidget.reveal(focused); return true; } } @@ -778,31 +892,113 @@ class RenameCandidateListView { } } -/** - * @remarks lazily creates the DOM node - */ -class RenameInput implements IDisposable { +class InputWithButton implements IDisposable { - private _domNode: HTMLInputElement | undefined; + private _buttonState: 'sparkle' | 'stop' | undefined; - private readonly _onDidChange = new Emitter(); - public readonly onDidChange = this._onDidChange.event; + private _domNode: HTMLDivElement | undefined; + private _inputNode: HTMLInputElement | undefined; + private _buttonNode: HTMLElement | undefined; + private _buttonHover: IManagedHover | undefined; + private _buttonGenHoverText: string | undefined; + private _buttonCancelHoverText: string | undefined; + private _sparkleIcon: HTMLElement | undefined; + private _stopIcon: HTMLElement | undefined; - private _disposables = new DisposableStore(); + private readonly _onDidInputChange = new Emitter(); + public readonly onDidInputChange = this._onDidInputChange.event; + + private readonly _disposables = new DisposableStore(); get domNode() { if (!this._domNode) { - this._domNode = document.createElement('input'); - this._domNode.className = 'rename-input'; - this._domNode.type = 'text'; - this._domNode.setAttribute('aria-label', localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); - this._disposables.add(dom.addDisposableListener(this._domNode, 'input', () => this._onDidChange.fire())); + + this._domNode = document.createElement('div'); + this._domNode.className = 'rename-input-with-button'; + this._domNode.style.display = 'flex'; + this._domNode.style.flexDirection = 'row'; + this._domNode.style.alignItems = 'center'; + + this._inputNode = document.createElement('input'); + this._inputNode.className = 'rename-input'; + this._inputNode.type = 'text'; + this._inputNode.style.border = 'none'; + this._inputNode.setAttribute('aria-label', nls.localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); + + this._domNode.appendChild(this._inputNode); + + this._buttonNode = document.createElement('div'); + this._buttonNode.className = 'rename-suggestions-button'; + this._buttonNode.setAttribute('tabindex', '0'); + + this._buttonGenHoverText = nls.localize('generateRenameSuggestionsButton', "Generate new name suggestions"); + this._buttonCancelHoverText = nls.localize('cancelRenameSuggestionsButton', "Cancel"); + this._buttonHover = getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('element'), this._buttonNode, this._buttonGenHoverText); + this._disposables.add(this._buttonHover); + + this._domNode.appendChild(this._buttonNode); + + // notify if selection changes to cancel request to rename-suggestion providers + + this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.INPUT, () => this._onDidInputChange.fire())); + this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.KEY_DOWN, (e) => { + const keyEvent = new StandardKeyboardEvent(e); + if (keyEvent.keyCode === KeyCode.LeftArrow || keyEvent.keyCode === KeyCode.RightArrow) { + this._onDidInputChange.fire(); + } + })); + this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.CLICK, () => this._onDidInputChange.fire())); + + // focus "container" border instead of input box + + this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.FOCUS, () => { + this.domNode.style.outlineWidth = '1px'; + this.domNode.style.outlineStyle = 'solid'; + this.domNode.style.outlineOffset = '-1px'; + this.domNode.style.outlineColor = 'var(--vscode-focusBorder)'; + })); + this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.BLUR, () => { + this.domNode.style.outline = 'none'; + })); } return this._domNode; } + get input() { + assertType(this._inputNode); + return this._inputNode; + } + + get button() { + assertType(this._buttonNode); + return this._buttonNode; + } + + get buttonState() { + return this._buttonState; + } + + setSparkleButton() { + this._buttonState = 'sparkle'; + this._sparkleIcon ??= renderIcon(Codicon.sparkle); + dom.clearNode(this.button); + this.button.appendChild(this._sparkleIcon); + this.button.setAttribute('aria-label', 'Generating new name suggestions'); + this._buttonHover?.update(this._buttonGenHoverText); + this.input.focus(); + } + + setStopButton() { + this._buttonState = 'stop'; + this._stopIcon ??= renderIcon(Codicon.primitiveSquare); + dom.clearNode(this.button); + this.button.appendChild(this._stopIcon); + this.button.setAttribute('aria-label', 'Cancel generating new name suggestions'); + this._buttonHover?.update(this._buttonCancelHoverText); + this.input.focus(); + } + dispose(): void { - this._onDidChange.dispose(); this._disposables.dispose(); } } @@ -818,6 +1014,7 @@ class RenameCandidateView { constructor(parent: HTMLElement, fontInfo: FontInfo) { this._domNode = document.createElement('div'); + this._domNode.className = 'rename-box rename-candidate'; this._domNode.style.display = `flex`; this._domNode.style.columnGap = `5px`; this._domNode.style.alignItems = `center`; @@ -836,7 +1033,7 @@ class RenameCandidateView { iconContainer.appendChild(this._icon); this._label = document.createElement('div'); - applyFontInfo(this._label, fontInfo); + domFontInfo.applyFontInfo(this._label, fontInfo); this._domNode.appendChild(this._label); parent.appendChild(this._domNode); diff --git a/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts index f3296062d81..2a60e180cfd 100644 --- a/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts +++ b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts @@ -82,6 +82,12 @@ export class SectionHeaderDetector extends Disposable implements IEditorContribu this.computeSectionHeaders.schedule(); })); + this._register(editor.onDidChangeModelTokens((e) => { + if (!this.computeSectionHeaders.isScheduled()) { + this.computeSectionHeaders.schedule(1000); + } + })); + this.computeSectionHeaders = this._register(new RunOnceScheduler(() => { this.findSectionHeaders(); }, 250)); diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts index c2d9a57c202..9eb8d2f0006 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -14,6 +14,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { Range } from 'vs/editor/common/core/range'; import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ITextModel } from 'vs/editor/common/model'; import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -29,6 +30,7 @@ import { TestTextResourcePropertiesService } from 'vs/editor/test/common/service import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NullLogService } from 'vs/platform/log/common/log'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { ColorScheme } from 'vs/platform/theme/common/theme'; @@ -50,12 +52,14 @@ suite('ModelSemanticColoring', () => { languageFeaturesService = new LanguageFeaturesService(); languageService = disposables.add(new LanguageService(false)); const semanticTokensStylingService = disposables.add(new SemanticTokensStylingService(themeService, logService, languageService)); + const instantiationService = new TestInstantiationService(); + instantiationService.set(ILanguageService, languageService); + instantiationService.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); modelService = disposables.add(new ModelService( configService, new TestTextResourcePropertiesService(configService), new UndoRedoService(new TestDialogService(), new TestNotificationService()), - languageService, - new TestLanguageConfigurationService(), + instantiationService )); const envService = new class extends mock() { override isBuilt: boolean = true; diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts index da8dc2f222d..6ac97ecde96 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts index dc9e9767d81..1de9179cc28 100644 --- a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index 0290c450113..e066ef8ee56 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -331,7 +331,7 @@ registerEditorCommand(new CommandCtor({ handler: ctrl => ctrl.next(), kbOpts: { weight: KeybindingWeight.EditorContrib + 30, - kbExpr: EditorContextKeys.editorTextFocus, + kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.Tab } })); @@ -341,7 +341,7 @@ registerEditorCommand(new CommandCtor({ handler: ctrl => ctrl.prev(), kbOpts: { weight: KeybindingWeight.EditorContrib + 30, - kbExpr: EditorContextKeys.editorTextFocus, + kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.Shift | KeyCode.Tab } })); @@ -351,7 +351,7 @@ registerEditorCommand(new CommandCtor({ handler: ctrl => ctrl.cancel(true), kbOpts: { weight: KeybindingWeight.EditorContrib + 30, - kbExpr: EditorContextKeys.editorTextFocus, + kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] } diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts index e0f11350e89..236ad70f27d 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts index 1c9c3a80cdb..5b9a5e434f8 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts index ee2f1f1d24f..bbf24168409 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from 'vs/editor/contrib/snippet/browser/snippetParser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index 61061846724..0a9a3c76462 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts index 53a9b272757..b8ea4e52342 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import * as sinon from 'sinon'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { sep } from 'vs/base/common/path'; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 8afc9c241cf..276082256f6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -61,9 +61,10 @@ .monaco-editor .sticky-widget { width: 100%; - box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; + box-shadow: var(--vscode-editorStickyScroll-shadow) 0 4px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); + right: initial !important; } .monaco-editor .sticky-widget.peek { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts index 1e49f122979..bbeb000ad8c 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollActions.ts @@ -24,6 +24,9 @@ export class ToggleStickyScroll extends Action2 { ...localize2('toggleEditorStickyScroll', "Toggle Editor Sticky Scroll"), mnemonicTitle: localize({ key: 'mitoggleStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Editor Sticky Scroll"), }, + metadata: { + description: localize2('toggleEditorStickyScroll.description', "Toggle/enable the editor sticky scroll which shows the nested scopes at the top of the viewport"), + }, category: Categories.View, toggled: { condition: ContextKeyExpr.equals('config.editor.stickyScroll.enabled', true), @@ -53,7 +56,7 @@ export class FocusStickyScroll extends EditorAction2 { super({ id: 'editor.action.focusStickyScroll', title: { - ...localize2('focusStickyScroll', "Focus Sticky Scroll"), + ...localize2('focusStickyScroll', "Focus on the editor sticky scroll"), mnemonicTitle: localize({ key: 'mifocusStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Focus Sticky Scroll"), }, precondition: ContextKeyExpr.and(ContextKeyExpr.has('config.editor.stickyScroll.enabled'), EditorContextKeys.stickyScrollVisible), @@ -72,7 +75,7 @@ export class SelectNextStickyScrollLine extends EditorAction2 { constructor() { super({ id: 'editor.action.selectNextStickyScrollLine', - title: localize2('selectNextStickyScrollLine.title', "Select next sticky scroll line"), + title: localize2('selectNextStickyScrollLine.title', "Select the next editor sticky scroll line"), precondition: EditorContextKeys.stickyScrollFocused.isEqualTo(true), keybinding: { weight, @@ -90,7 +93,7 @@ export class SelectPreviousStickyScrollLine extends EditorAction2 { constructor() { super({ id: 'editor.action.selectPreviousStickyScrollLine', - title: localize2('selectPreviousStickyScrollLine.title', "Select previous sticky scroll line"), + title: localize2('selectPreviousStickyScrollLine.title', "Select the previous sticky scroll line"), precondition: EditorContextKeys.stickyScrollFocused.isEqualTo(true), keybinding: { weight, @@ -108,7 +111,7 @@ export class GoToStickyScrollLine extends EditorAction2 { constructor() { super({ id: 'editor.action.goToFocusedStickyScrollLine', - title: localize2('goToFocusedStickyScrollLine.title', "Go to focused sticky scroll line"), + title: localize2('goToFocusedStickyScrollLine.title', "Go to the focused sticky scroll line"), precondition: EditorContextKeys.stickyScrollFocused.isEqualTo(true), keybinding: { weight, diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index 492524e64b7..65824a12798 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -7,7 +7,7 @@ import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecy import { IActiveCodeEditor, ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, RenderLineNumbersType, ConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import { StickyScrollWidget, StickyScrollWidgetState } from './stickyScrollWidget'; import { IStickyLineCandidateProvider, StickyLineCandidateProvider } from './stickyScrollProvider'; import { IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; @@ -51,7 +51,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib private readonly _sessionStore: DisposableStore = new DisposableStore(); private _widgetState: StickyScrollWidgetState; - private _foldingModel: FoldingModel | null = null; + private _foldingModel: FoldingModel | undefined; private _maxStickyLines: number = Number.MAX_SAFE_INTEGER; private _stickyRangeProjectedOnEditor: IRange | undefined; @@ -67,7 +67,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib private _positionRevealed = false; private _onMouseDown = false; private _endLineNumbers: number[] = []; - private _showEndForLine: number | null = null; + private _showEndForLine: number | undefined; + private _minRebuildFromLine: number | undefined; constructor( private readonly _editor: ICodeEditor, @@ -84,19 +85,12 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._register(this._stickyScrollWidget); this._register(this._stickyLineCandidateProvider); - this._widgetState = new StickyScrollWidgetState([], [], 0); + this._widgetState = StickyScrollWidgetState.Empty; this._onDidResize(); this._readConfiguration(); const stickyScrollDomNode = this._stickyScrollWidget.getDomNode(); this._register(this._editor.onDidChangeConfiguration(e => { - if ( - e.hasChanged(EditorOption.stickyScroll) - || e.hasChanged(EditorOption.minimap) - || e.hasChanged(EditorOption.lineHeight) - || e.hasChanged(EditorOption.showFoldingControls) - ) { - this._readConfiguration(); - } + this._readConfigurationChange(e); })); this._register(dom.addDisposableListener(stickyScrollDomNode, dom.EventType.CONTEXT_MENU, async (event: MouseEvent) => { this._onContextMenu(dom.getWindow(stickyScrollDomNode), event); @@ -298,14 +292,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._renderStickyScroll(); return; } - if (this._showEndForLine !== null) { - this._showEndForLine = null; + if (this._showEndForLine !== undefined) { + this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._register(dom.addDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_LEAVE, (e) => { - if (this._showEndForLine !== null) { - this._showEndForLine = null; + if (this._showEndForLine !== undefined) { + this._showEndForLine = undefined; this._renderStickyScroll(); } })); @@ -330,7 +324,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib let currentHTMLChild: HTMLElement; - getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, this._editor.getModel(), new Position(range.startLineNumber, range.startColumn + 1), cancellationToken.token).then((candidateDefinitions => { + getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, this._editor.getModel(), new Position(range.startLineNumber, range.startColumn + 1), false, cancellationToken.token).then((candidateDefinitions => { if (cancellationToken.token.isCancellationRequested) { return; } @@ -422,14 +416,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._editor.addOverlayWidget(this._stickyScrollWidget); this._sessionStore.add(this._editor.onDidScrollChange((e) => { if (e.scrollTopChanged) { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize())); this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e))); this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(); })); this._enabled = true; @@ -438,12 +432,28 @@ export class StickyScrollController extends Disposable implements IEditorContrib const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers); if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(0); })); } } + private _readConfigurationChange(event: ConfigurationChangedEvent) { + if ( + event.hasChanged(EditorOption.stickyScroll) + || event.hasChanged(EditorOption.minimap) + || event.hasChanged(EditorOption.lineHeight) + || event.hasChanged(EditorOption.showFoldingControls) + || event.hasChanged(EditorOption.lineNumbers) + ) { + this._readConfiguration(); + } + + if (event.hasChanged(EditorOption.lineNumbers)) { + this._renderStickyScroll(0); + } + } + private _needsUpdate(event: IModelTokensChangedEvent) { const stickyLineNumbers = this._stickyScrollWidget.getCurrentLines(); for (const stickyLineNumber of stickyLineNumbers) { @@ -470,32 +480,29 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._maxStickyLines = Math.round(theoreticalLines * .25); } - private async _renderStickyScroll(rebuildFromLine?: number) { + private async _renderStickyScroll(rebuildFromLine?: number): Promise { const model = this._editor.getModel(); if (!model || model.isTooLargeForTokenization()) { - this._foldingModel = null; - this._stickyScrollWidget.setState(undefined, null); + this._resetState(); return; } - const stickyLineVersion = this._stickyLineCandidateProvider.getVersionId(); - if (stickyLineVersion === undefined || stickyLineVersion === model.getVersionId()) { - this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? null; - this._widgetState = this.findScrollWidgetState(); - this._stickyScrollVisibleContextKey.set(!(this._widgetState.startLineNumbers.length === 0)); - + const nextRebuildFromLine = this._updateAndGetMinRebuildFromLine(rebuildFromLine); + const stickyWidgetVersion = this._stickyLineCandidateProvider.getVersionId(); + const shouldUpdateState = stickyWidgetVersion === undefined || stickyWidgetVersion === model.getVersionId(); + if (shouldUpdateState) { if (!this._focused) { - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); } else { // Suppose that previously the sticky scroll widget had height 0, then if there are visible lines, set the last line as focused if (this._focusedStickyElementIndex === -1) { - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1; if (this._focusedStickyElementIndex !== -1) { this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } } else { const focusedStickyElementLineNumber = this._stickyScrollWidget.lineNumbers[this._focusedStickyElementIndex]; - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); // Suppose that after setting the state, there are no sticky lines, set the focused index to -1 if (this._stickyScrollWidget.lineNumberCount === 0) { this._focusedStickyElementIndex = -1; @@ -514,6 +521,31 @@ export class StickyScrollController extends Disposable implements IEditorContrib } } + private _updateAndGetMinRebuildFromLine(rebuildFromLine: number | undefined): number | undefined { + if (rebuildFromLine !== undefined) { + const minRebuildFromLineOrInfinity = this._minRebuildFromLine !== undefined ? this._minRebuildFromLine : Infinity; + this._minRebuildFromLine = Math.min(rebuildFromLine, minRebuildFromLineOrInfinity); + } + return this._minRebuildFromLine; + } + + private async _updateState(rebuildFromLine?: number): Promise { + this._minRebuildFromLine = undefined; + this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? undefined; + this._widgetState = this.findScrollWidgetState(); + const stickyWidgetHasLines = this._widgetState.startLineNumbers.length > 0; + this._stickyScrollVisibleContextKey.set(stickyWidgetHasLines); + this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + } + + private async _resetState(): Promise { + this._minRebuildFromLine = undefined; + this._foldingModel = undefined; + this._widgetState = StickyScrollWidgetState.Empty; + this._stickyScrollVisibleContextKey.set(false); + this._stickyScrollWidget.setState(undefined, undefined); + } + findScrollWidgetState(): StickyScrollWidgetState { const lineHeight: number = this._editor.getOption(EditorOption.lineHeight); const maxNumberStickyLines = Math.min(this._maxStickyLines, this._editor.getOption(EditorOption.stickyScroll).maxLineCount); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts index 231052024c6..22cfdb013a7 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts @@ -52,8 +52,8 @@ export class StickyModelProvider extends Disposable implements IStickyModelProvi constructor( private readonly _editor: IActiveCodeEditor, onProviderUpdate: () => void, - @IInstantiationService readonly _languageConfigurationService: ILanguageConfigurationService, - @ILanguageFeaturesService readonly _languageFeaturesService: ILanguageFeaturesService, + @IInstantiationService _languageConfigurationService: ILanguageConfigurationService, + @ILanguageFeaturesService _languageFeaturesService: ILanguageFeaturesService, ) { super(); @@ -308,7 +308,6 @@ class StickyModelFromCandidateOutlineProvider extends StickyModelCandidateProvid return res; } } - } abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandidateProvider { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index bdcaafb4891..27d3d8e1a58 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -9,7 +9,7 @@ import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -35,6 +35,10 @@ export class StickyScrollWidgetState { && equals(this.startLineNumbers, other.startLineNumbers) && equals(this.endLineNumbers, other.endLineNumbers); } + + static get Empty() { + return new StickyScrollWidgetState([], [], 0); + } } const _ttPolicy = createTrustedTypesPolicy('stickyScrollViewLayer', { createHTML: value => value }); @@ -126,7 +130,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return this._lineNumbers; } - setState(_state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | null, _rebuildFromLine?: number): void { + setState(_state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | undefined, _rebuildFromLine?: number): void { if (_rebuildFromLine === undefined && ((!this._previousState && !_state) || (this._previousState && this._previousState.equals(_state))) ) { @@ -205,7 +209,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } } - private async _renderRootNode(state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | null, rebuildFromLine: number): Promise { + private async _renderRootNode(state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | undefined, rebuildFromLine: number): Promise { this._clearStickyLinesFromLine(rebuildFromLine); if (!state) { return; @@ -258,7 +262,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { })); } - private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | null, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { + private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { const viewModel = this._editor._getViewModel(); if (!viewModel) { return; @@ -358,7 +362,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return stickyLine; } - private _renderFoldingIconForLine(foldingModel: FoldingModel | null, line: number): StickyFoldingIcon | undefined { + private _renderFoldingIconForLine(foldingModel: FoldingModel | undefined, line: number): StickyFoldingIcon | undefined { const showFoldingControls: 'mouseover' | 'always' | 'never' = this._editor.getOption(EditorOption.showFoldingControls); if (!foldingModel || showFoldingControls === 'never') { return; @@ -387,7 +391,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { getPosition(): IOverlayWidgetPosition | null { return { - preference: null + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 10, }; } diff --git a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts index 04e852765ca..aaefcb65f3d 100644 --- a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts +++ b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { StickyScrollController } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollController'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index fb837e61172..0e29ea762bc 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -509,15 +509,14 @@ export class SuggestController implements IEditorContribution { // clear only now - after all tasks are done Promise.all(tasks).finally(() => { - this._reportSuggestionAcceptedTelemetry(item, model, isResolved, _commandExectionDuration, _additionalEditsAppliedAsync); + this._reportSuggestionAcceptedTelemetry(item, model, isResolved, _commandExectionDuration, _additionalEditsAppliedAsync, event.index); this.model.clear(); cts.dispose(); }); } - private _reportSuggestionAcceptedTelemetry(item: CompletionItem, model: ITextModel, itemResolved: boolean, commandExectionDuration: number, additionalEditsAppliedAsync: number) { - + private _reportSuggestionAcceptedTelemetry(item: CompletionItem, model: ITextModel, itemResolved: boolean, commandExectionDuration: number, additionalEditsAppliedAsync: number, index: number): void { if (Math.floor(Math.random() * 100) === 0) { // throttle telemetry event because accepting completions happens a lot return; @@ -529,6 +528,8 @@ export class SuggestController implements IEditorContribution { resolveInfo: number; resolveDuration: number; commandDuration: number; additionalEditsAsync: number; + index: number; + label: string; }; type AcceptedSuggestionClassification = { owner: 'jrieken'; @@ -538,11 +539,13 @@ export class SuggestController implements IEditorContribution { basenameHash: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Hash of the basename of the file into which the completion was inserted' }; fileExtension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension of the file into which the completion was inserted' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Language type of the file into which the completion was inserted' }; - kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The completion item kind' }; - resolveInfo: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'If the item was inserted before resolving was done' }; - resolveDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'How long resolving took to finish' }; - commandDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'How long a completion item command took' }; - additionalEditsAsync: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Info about asynchronously applying additional edits' }; + kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The completion item kind' }; + resolveInfo: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the item was inserted before resolving was done' }; + resolveDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How long resolving took to finish' }; + commandDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How long a completion item command took' }; + additionalEditsAsync: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Info about asynchronously applying additional edits' }; + index: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The order of the completion item in the list.' }; + label: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the completion item.' }; }; this._telemetryService.publicLog2('suggest.acceptedSuggestion', { @@ -555,7 +558,9 @@ export class SuggestController implements IEditorContribution { resolveInfo: !item.provider.resolveCompletionItem ? -1 : itemResolved ? 1 : 0, resolveDuration: item.resolveDuration, commandDuration: commandExectionDuration, - additionalEditsAsync: additionalEditsAppliedAsync + additionalEditsAsync: additionalEditsAppliedAsync, + index, + label: item.textLabel }); } diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 2eeb94d99b6..39c98256f30 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -39,15 +39,15 @@ import { status } from 'vs/base/browser/ui/aria/aria'; /** * Suggest widget colors */ -registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); -registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); -const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hcDark: editorForeground, hcLight: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); -registerColor('editorSuggestWidget.selectedForeground', { dark: quickInputListFocusForeground, light: quickInputListFocusForeground, hcDark: quickInputListFocusForeground, hcLight: quickInputListFocusForeground }, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); -registerColor('editorSuggestWidget.selectedIconForeground', { dark: quickInputListFocusIconForeground, light: quickInputListFocusIconForeground, hcDark: quickInputListFocusIconForeground, hcLight: quickInputListFocusIconForeground }, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.')); -export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: quickInputListFocusBackground, light: quickInputListFocusBackground, hcDark: quickInputListFocusBackground, hcLight: quickInputListFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); -registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); -registerColor('editorSuggestWidget.focusHighlightForeground', { dark: listFocusHighlightForeground, light: listFocusHighlightForeground, hcDark: listFocusHighlightForeground, hcLight: listFocusHighlightForeground }, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); -registerColor('editorSuggestWidgetStatus.foreground', { dark: transparent(editorSuggestWidgetForeground, .5), light: transparent(editorSuggestWidgetForeground, .5), hcDark: transparent(editorSuggestWidgetForeground, .5), hcLight: transparent(editorSuggestWidgetForeground, .5) }, nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.')); +registerColor('editorSuggestWidget.background', editorWidgetBackground, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); +registerColor('editorSuggestWidget.border', editorWidgetBorder, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); +const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', editorForeground, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); +registerColor('editorSuggestWidget.selectedForeground', quickInputListFocusForeground, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); +registerColor('editorSuggestWidget.selectedIconForeground', quickInputListFocusIconForeground, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.')); +export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', quickInputListFocusBackground, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); +registerColor('editorSuggestWidget.highlightForeground', listHighlightForeground, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); +registerColor('editorSuggestWidget.focusHighlightForeground', listFocusHighlightForeground, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); +registerColor('editorSuggestWidgetStatus.foreground', transparent(editorSuggestWidgetForeground, .5), nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.')); const enum State { Hidden, @@ -55,7 +55,8 @@ const enum State { Empty, Open, Frozen, - Details + Details, + onDetailsKeyDown } export interface ISelectedSuggestion { diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts index 25d19c054d2..4a1df5b9ce8 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts @@ -6,31 +6,12 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction } from 'vs/base/common/actions'; -import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { localize } from 'vs/nls'; -import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -class StatusBarViewItem extends MenuEntryActionViewItem { - - protected override updateLabel() { - const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService); - if (!kb) { - return super.updateLabel(); - } - if (this.label) { - this.label.textContent = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', this._action.label, StatusBarViewItem.symbolPrintEnter(kb)); - } - } - - static symbolPrintEnter(kb: ResolvedKeybinding) { - return kb.getLabel()?.replace(/\benter\b/gi, '\u23CE'); - } -} - export class SuggestWidgetStatus { readonly element: HTMLElement; @@ -49,7 +30,7 @@ export class SuggestWidgetStatus { this.element = dom.append(container, dom.$('.suggest-status-bar')); const actionViewItemProvider = (action => { - return action instanceof MenuItemAction ? instantiationService.createInstance(StatusBarViewItem, action, undefined) : undefined; + return action instanceof MenuItemAction ? instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { useComma: true }) : undefined; }); this._leftActions = new ActionBar(this.element, { actionViewItemProvider }); this._rightActions = new ActionBar(this.element, { actionViewItemProvider }); diff --git a/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts index cb08e56fb7f..823d6865a84 100644 --- a/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOptions, InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts index d4df28ea332..b2e9fc03577 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts index 617019ff5cf..64123de47db 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts index 1aaca84d484..91d42374b1f 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts index 3bbc8c58838..57d0d030d48 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IPosition } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index 2bbfe524bfb..5c673c0f355 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts index 04f7bce1cc1..15f238b1627 100644 --- a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts index 64ccb1bc618..7d6fe4b73f6 100644 --- a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts +++ b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts @@ -7,19 +7,9 @@ import 'vs/css!./symbolIcons'; import { localize } from 'vs/nls'; import { foreground, registerColor } from 'vs/platform/theme/common/colorRegistry'; -export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.arrayForeground', 'The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', foreground, localize('symbolIcon.arrayForeground', 'The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_BOOLEAN_FOREGROUND = registerColor('symbolIcon.booleanForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.booleanForeground', 'The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_BOOLEAN_FOREGROUND = registerColor('symbolIcon.booleanForeground', foreground, localize('symbolIcon.booleanForeground', 'The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_CLASS_FOREGROUND = registerColor('symbolIcon.classForeground', { dark: '#EE9D28', @@ -28,19 +18,9 @@ export const SYMBOL_ICON_CLASS_FOREGROUND = registerColor('symbolIcon.classForeg hcLight: '#D67E00' }, localize('symbolIcon.classForeground', 'The foreground color for class symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_COLOR_FOREGROUND = registerColor('symbolIcon.colorForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.colorForeground', 'The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_COLOR_FOREGROUND = registerColor('symbolIcon.colorForeground', foreground, localize('symbolIcon.colorForeground', 'The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_CONSTANT_FOREGROUND = registerColor('symbolIcon.constantForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.constantForeground', 'The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_CONSTANT_FOREGROUND = registerColor('symbolIcon.constantForeground', foreground, localize('symbolIcon.constantForeground', 'The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_CONSTRUCTOR_FOREGROUND = registerColor('symbolIcon.constructorForeground', { dark: '#B180D7', @@ -77,19 +57,9 @@ export const SYMBOL_ICON_FIELD_FOREGROUND = registerColor('symbolIcon.fieldForeg hcLight: '#007ACC' }, localize('symbolIcon.fieldForeground', 'The foreground color for field symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_FILE_FOREGROUND = registerColor('symbolIcon.fileForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.fileForeground', 'The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_FILE_FOREGROUND = registerColor('symbolIcon.fileForeground', foreground, localize('symbolIcon.fileForeground', 'The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('symbolIcon.folderForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.folderForeground', 'The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('symbolIcon.folderForeground', foreground, localize('symbolIcon.folderForeground', 'The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_FUNCTION_FOREGROUND = registerColor('symbolIcon.functionForeground', { dark: '#B180D7', @@ -105,19 +75,9 @@ export const SYMBOL_ICON_INTERFACE_FOREGROUND = registerColor('symbolIcon.interf hcLight: '#007ACC' }, localize('symbolIcon.interfaceForeground', 'The foreground color for interface symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_KEY_FOREGROUND = registerColor('symbolIcon.keyForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.keyForeground', 'The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_KEY_FOREGROUND = registerColor('symbolIcon.keyForeground', foreground, localize('symbolIcon.keyForeground', 'The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_KEYWORD_FOREGROUND = registerColor('symbolIcon.keywordForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.keywordForeground', 'The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_KEYWORD_FOREGROUND = registerColor('symbolIcon.keywordForeground', foreground, localize('symbolIcon.keywordForeground', 'The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_METHOD_FOREGROUND = registerColor('symbolIcon.methodForeground', { dark: '#B180D7', @@ -126,110 +86,35 @@ 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_MODULE_FOREGROUND = registerColor('symbolIcon.moduleForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.moduleForeground', 'The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and 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', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.namespaceForeground', 'The foreground color for namespace 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.')); -export const SYMBOL_ICON_NULL_FOREGROUND = registerColor('symbolIcon.nullForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.nullForeground', 'The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_NULL_FOREGROUND = registerColor('symbolIcon.nullForeground', foreground, localize('symbolIcon.nullForeground', 'The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_NUMBER_FOREGROUND = registerColor('symbolIcon.numberForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.numberForeground', 'The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_NUMBER_FOREGROUND = registerColor('symbolIcon.numberForeground', foreground, localize('symbolIcon.numberForeground', 'The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_OBJECT_FOREGROUND = registerColor('symbolIcon.objectForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.objectForeground', 'The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_OBJECT_FOREGROUND = registerColor('symbolIcon.objectForeground', foreground, localize('symbolIcon.objectForeground', 'The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_OPERATOR_FOREGROUND = registerColor('symbolIcon.operatorForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.operatorForeground', 'The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_OPERATOR_FOREGROUND = registerColor('symbolIcon.operatorForeground', foreground, localize('symbolIcon.operatorForeground', 'The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_PACKAGE_FOREGROUND = registerColor('symbolIcon.packageForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.packageForeground', 'The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_PACKAGE_FOREGROUND = registerColor('symbolIcon.packageForeground', foreground, localize('symbolIcon.packageForeground', 'The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_PROPERTY_FOREGROUND = registerColor('symbolIcon.propertyForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.propertyForeground', 'The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_PROPERTY_FOREGROUND = registerColor('symbolIcon.propertyForeground', foreground, localize('symbolIcon.propertyForeground', 'The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_REFERENCE_FOREGROUND = registerColor('symbolIcon.referenceForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.referenceForeground', 'The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_REFERENCE_FOREGROUND = registerColor('symbolIcon.referenceForeground', foreground, localize('symbolIcon.referenceForeground', 'The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_SNIPPET_FOREGROUND = registerColor('symbolIcon.snippetForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.snippetForeground', 'The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_SNIPPET_FOREGROUND = registerColor('symbolIcon.snippetForeground', foreground, localize('symbolIcon.snippetForeground', 'The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_STRING_FOREGROUND = registerColor('symbolIcon.stringForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.stringForeground', 'The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_STRING_FOREGROUND = registerColor('symbolIcon.stringForeground', foreground, localize('symbolIcon.stringForeground', 'The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_STRUCT_FOREGROUND = registerColor('symbolIcon.structForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.structForeground', 'The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_STRUCT_FOREGROUND = registerColor('symbolIcon.structForeground', foreground, localize('symbolIcon.structForeground', 'The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_TEXT_FOREGROUND = registerColor('symbolIcon.textForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.textForeground', 'The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_TEXT_FOREGROUND = registerColor('symbolIcon.textForeground', foreground, localize('symbolIcon.textForeground', 'The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_TYPEPARAMETER_FOREGROUND = registerColor('symbolIcon.typeParameterForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.typeParameterForeground', 'The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_TYPEPARAMETER_FOREGROUND = registerColor('symbolIcon.typeParameterForeground', foreground, localize('symbolIcon.typeParameterForeground', 'The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_UNIT_FOREGROUND = registerColor('symbolIcon.unitForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.unitForeground', 'The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_UNIT_FOREGROUND = registerColor('symbolIcon.unitForeground', foreground, localize('symbolIcon.unitForeground', 'The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_VARIABLE_FOREGROUND = registerColor('symbolIcon.variableForeground', { dark: '#75BEFF', diff --git a/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts b/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts index 64f4b309e8a..7578cd62f8e 100644 --- a/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts +++ b/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts @@ -24,6 +24,9 @@ export class ToggleTabFocusModeAction extends Action2 { mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyM }, weight: KeybindingWeight.EditorContrib }, + metadata: { + description: nls.localize2('tabMovesFocusDescriptions', "Determines whether the tab key moves focus around the workbench or inserts the tab character in the current editor. This is also called tab trapping, tab navigation, or tab focus mode."), + }, f1: true }); } diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index f44fc76e0bd..eae47ac2d71 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -7,7 +7,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { InvisibleCharacters, isBasicASCII } from 'vs/base/common/strings'; import 'vs/css!./unicodeHighlighter'; @@ -22,7 +22,7 @@ import { UnicodeHighlighterOptions, UnicodeHighlighterReason, UnicodeHighlighter import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { isModelDecorationInComment, isModelDecorationInString, isModelDecorationVisible } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { MarkdownHover, renderMarkdownHovers } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; import { BannerController } from 'vs/editor/contrib/unicodeHighlighter/browser/bannerController'; import * as nls from 'vs/nls'; @@ -506,9 +506,13 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa return result; } - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IDisposable { + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IRenderedHoverParts { return renderMarkdownHovers(context, hoverParts, this._editor, this._languageService, this._openerService); } + + public getAccessibleContent(hoverPart: MarkdownHover): string { + return hoverPart.contents.map(c => c.value).join('\n'); + } } function codePointToHex(codePoint: number): string { diff --git a/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts b/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts index b3825b0f05d..79ec7ad02e6 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts @@ -13,13 +13,13 @@ import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/ const wordHighlightBackground = registerColor('editor.wordHighlightBackground', { dark: '#575757B8', light: '#57575740', hcDark: null, hcLight: null }, nls.localize('wordHighlight', 'Background color of a symbol during read-access, like reading a variable. The color must not be opaque so as not to hide underlying decorations.'), true); registerColor('editor.wordHighlightStrongBackground', { dark: '#004972B8', light: '#0e639c40', hcDark: null, hcLight: null }, nls.localize('wordHighlightStrong', 'Background color of a symbol during write-access, like writing to a variable. The color must not be opaque so as not to hide underlying decorations.'), true); -registerColor('editor.wordHighlightTextBackground', { light: wordHighlightBackground, dark: wordHighlightBackground, hcDark: wordHighlightBackground, hcLight: wordHighlightBackground }, nls.localize('wordHighlightText', 'Background color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); +registerColor('editor.wordHighlightTextBackground', wordHighlightBackground, nls.localize('wordHighlightText', 'Background color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); const wordHighlightBorder = registerColor('editor.wordHighlightBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('wordHighlightBorder', 'Border color of a symbol during read-access, like reading a variable.')); registerColor('editor.wordHighlightStrongBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('wordHighlightStrongBorder', 'Border color of a symbol during write-access, like writing to a variable.')); -registerColor('editor.wordHighlightTextBorder', { light: wordHighlightBorder, dark: wordHighlightBorder, hcDark: wordHighlightBorder, hcLight: wordHighlightBorder }, nls.localize('wordHighlightTextBorder', "Border color of a textual occurrence for a symbol.")); -const overviewRulerWordHighlightForeground = registerColor('editorOverviewRuler.wordHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, nls.localize('overviewRulerWordHighlightForeground', 'Overview ruler marker color for symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); -const overviewRulerWordHighlightStrongForeground = registerColor('editorOverviewRuler.wordHighlightStrongForeground', { dark: '#C0A0C0CC', light: '#C0A0C0CC', hcDark: '#C0A0C0CC', hcLight: '#C0A0C0CC' }, nls.localize('overviewRulerWordHighlightStrongForeground', 'Overview ruler marker color for write-access symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); -const overviewRulerWordHighlightTextForeground = registerColor('editorOverviewRuler.wordHighlightTextForeground', { dark: overviewRulerSelectionHighlightForeground, light: overviewRulerSelectionHighlightForeground, hcDark: overviewRulerSelectionHighlightForeground, hcLight: overviewRulerSelectionHighlightForeground }, nls.localize('overviewRulerWordHighlightTextForeground', 'Overview ruler marker color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); +registerColor('editor.wordHighlightTextBorder', wordHighlightBorder, nls.localize('wordHighlightTextBorder', "Border color of a textual occurrence for a symbol.")); +const overviewRulerWordHighlightForeground = registerColor('editorOverviewRuler.wordHighlightForeground', '#A0A0A0CC', nls.localize('overviewRulerWordHighlightForeground', 'Overview ruler marker color for symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); +const overviewRulerWordHighlightStrongForeground = registerColor('editorOverviewRuler.wordHighlightStrongForeground', '#C0A0C0CC', nls.localize('overviewRulerWordHighlightStrongForeground', 'Overview ruler marker color for write-access symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); +const overviewRulerWordHighlightTextForeground = registerColor('editorOverviewRuler.wordHighlightTextForeground', overviewRulerSelectionHighlightForeground, nls.localize('overviewRulerWordHighlightTextForeground', 'Overview ruler marker color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); const _WRITE_OPTIONS = ModelDecorationOptions.register({ description: 'word-highlight-strong', diff --git a/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts b/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts index 1d7a65695d3..adc31ecd8c0 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider.ts @@ -5,7 +5,7 @@ import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { DocumentHighlight, DocumentHighlightKind, MultiDocumentHighlightProvider, ProviderResult } from 'vs/editor/common/languages'; +import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProvider, MultiDocumentHighlightProvider, ProviderResult } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { Position } from 'vs/editor/common/core/position'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -14,10 +14,33 @@ import { ResourceMap } from 'vs/base/common/map'; import { LanguageFilter } from 'vs/editor/common/languageSelector'; -class TextualDocumentHighlightProvider implements MultiDocumentHighlightProvider { +class TextualDocumentHighlightProvider implements DocumentHighlightProvider, MultiDocumentHighlightProvider { selector: LanguageFilter = { language: '*' }; + provideDocumentHighlights(model: ITextModel, position: Position, token: CancellationToken): ProviderResult { + const result: DocumentHighlight[] = []; + + const word = model.getWordAtPosition({ + lineNumber: position.lineNumber, + column: position.column + }); + + if (!word) { + return Promise.resolve(result); + } + + if (model.isDisposed()) { + return; + } + + const matches = model.findMatches(word.word, true, false, true, USUAL_WORD_SEPARATORS, false); + return matches.map(m => ({ + range: m.range, + kind: DocumentHighlightKind.Text + })); + } + provideMultiDocumentHighlights(primaryModel: ITextModel, position: Position, otherModels: ITextModel[], token: CancellationToken): ProviderResult> { const result = new ResourceMap(); @@ -57,7 +80,7 @@ export class TextualMultiDocumentHighlightFeature extends Disposable { @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, ) { super(); - + this._register(languageFeaturesService.documentHighlightProvider.register('*', new TextualDocumentHighlightProvider())); this._register(languageFeaturesService.multiDocumentHighlightProvider.register('*', new TextualDocumentHighlightProvider())); } } diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index cae43742902..22bd3944e14 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; import { alert } from 'vs/base/browser/ui/aria/aria'; -import { CancelablePromise, createCancelablePromise, first, timeout } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, Delayer, first } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -23,7 +22,7 @@ import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/commo import { IDiffEditor, IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; -import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProvider, MultiDocumentHighlightProvider } from 'vs/editor/common/languages'; +import { DocumentHighlight, DocumentHighlightProvider, MultiDocumentHighlightProvider } from 'vs/editor/common/languages'; import { IModelDeltaDecoration, ITextModel, shouldSynchronizeModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { getHighlightDecorationOptions } from 'vs/editor/contrib/wordHighlighter/browser/highlightDecorations'; @@ -33,8 +32,9 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { Schemas } from 'vs/base/common/network'; import { ResourceMap } from 'vs/base/common/map'; import { score } from 'vs/editor/common/languageSelector'; -// import { TextualMultiDocumentHighlightFeature } from 'vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider'; -// import { registerEditorFeature } from 'vs/editor/common/editorFeatures'; +import { isEqual } from 'vs/base/common/resources'; +import { TextualMultiDocumentHighlightFeature } from 'vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider'; +import { registerEditorFeature } from 'vs/editor/common/editorFeatures'; const ctxHasWordHighlights = new RawContextKey('hasWordHighlights', false); @@ -43,11 +43,12 @@ export function getOccurrencesAtPosition(registry: LanguageFeatureRegistry(orderedByScore.map(provider => () => { return Promise.resolve(provider.provideDocumentHighlights(model, position, token)) .then(undefined, onUnexpectedExternalError); - }), arrays.isNonEmptyArray).then(result => { + }), (result): result is DocumentHighlight[] => result !== undefined && result !== null).then(result => { if (result) { const map = new ResourceMap(); map.set(model.uri, result); @@ -62,17 +63,17 @@ export function getOccurrencesAcrossMultipleModels(registry: LanguageFeatureRegi // in order of score ask the occurrences provider // until someone response with a good result - // (good = none empty array) + // (good = non undefined and non null ResourceMap) + // (result of size == 0 is valid, no highlights is a valid/expected result -- not a signal to fall back to other providers) return first | null | undefined>(orderedByScore.map(provider => () => { const filteredModels = otherModels.filter(otherModel => { return shouldSynchronizeModel(otherModel); }).filter(otherModel => { return score(provider.selector, otherModel.uri, otherModel.getLanguageId(), true, undefined, undefined) > 0; }); - return Promise.resolve(provider.provideMultiDocumentHighlights(model, position, filteredModels, token)) .then(undefined, onUnexpectedExternalError); - }), (t: ResourceMap | null | undefined): t is ResourceMap => t instanceof ResourceMap && t.size > 0); + }), (result): result is ResourceMap => result !== undefined && result !== null); } interface IOccurenceAtPositionRequest { @@ -183,76 +184,13 @@ class MultiModelOccurenceRequest extends OccurenceAtPositionRequest { } } -class TextualOccurenceRequest extends OccurenceAtPositionRequest { - - private readonly _otherModels: ITextModel[]; - private readonly _selectionIsEmpty: boolean; - private readonly _word: IWordAtPosition | null; - - constructor(model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]) { - super(model, selection, wordSeparators); - this._otherModels = otherModels; - this._selectionIsEmpty = selection.isEmpty(); - this._word = word; - } - - protected _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Promise> { - return timeout(250, token).then(() => { - const result = new ResourceMap(); - - let wordResult; - if (this._word) { - wordResult = this._word; - } else { - wordResult = model.getWordAtPosition(selection.getPosition()); - } - - if (!wordResult) { - return new ResourceMap(); - } - - const allModels = [model, ...this._otherModels]; - - for (const otherModel of allModels) { - if (otherModel.isDisposed()) { - continue; - } - - const matches = otherModel.findMatches(wordResult.word, true, false, true, wordSeparators, false); - const highlights = matches.map(m => ({ - range: m.range, - kind: DocumentHighlightKind.Text - })); - - if (highlights) { - result.set(otherModel.uri, highlights); - } - } - return result; - }); - } - - public override isValid(model: ITextModel, selection: Selection, decorations: IEditorDecorationsCollection): boolean { - const currentSelectionIsEmpty = selection.isEmpty(); - if (this._selectionIsEmpty !== currentSelectionIsEmpty) { - return false; - } - return super.isValid(model, selection, decorations); - } -} function computeOccurencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string): IOccurenceAtPositionRequest { - if (registry.has(model)) { - return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry); - } - return new TextualOccurenceRequest(model, selection, word, wordSeparators, []); + return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry); } function computeOccurencesMultiModel(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]): IOccurenceAtPositionRequest { - if (registry.has(model)) { - return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels); - } - return new TextualOccurenceRequest(model, selection, word, wordSeparators, otherModels); + return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels); } registerModelAndPositionCommand('_executeDocumentHighlights', async (accessor, model, position) => { @@ -266,11 +204,11 @@ class WordHighlighter { private readonly editor: IActiveCodeEditor; private readonly providers: LanguageFeatureRegistry; private readonly multiDocumentProviders: LanguageFeatureRegistry; - private occurrencesHighlight: string; private readonly model: ITextModel; private readonly decorations: IEditorDecorationsCollection; private readonly toUnhook = new DisposableStore(); private readonly codeEditorService: ICodeEditorService; + private occurrencesHighlight: string; private workerRequestTokenId: number = 0; private workerRequest: IOccurenceAtPositionRequest | null; @@ -283,7 +221,9 @@ class WordHighlighter { private readonly _hasWordHighlights: IContextKey; private _ignorePositionChangeEvent: boolean; - private static storedDecorations: ResourceMap = new ResourceMap(); + private readonly runDelayer: Delayer = this.toUnhook.add(new Delayer(50)); + + private static storedDecorationIDs: ResourceMap = new ResourceMap(); private static query: IWordHighlighterQuery | null = null; constructor(editor: IActiveCodeEditor, providers: LanguageFeatureRegistry, multiProviders: LanguageFeatureRegistry, contextKeyService: IContextKeyService, @ICodeEditorService codeEditorService: ICodeEditorService) { @@ -307,7 +247,7 @@ class WordHighlighter { return; } - this._onPositionChanged(e); + this.runDelayer.trigger(() => { this._onPositionChanged(e); }); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { if (this.occurrencesHighlight === 'off') { @@ -316,7 +256,7 @@ class WordHighlighter { } if (!this.workerRequest) { - this._run(); + this.runDelayer.trigger(() => { this._run(); }); } })); this.toUnhook.add(editor.onDidChangeModelContent((e) => { @@ -335,7 +275,22 @@ class WordHighlighter { const newValue = this.editor.getOption(EditorOption.occurrencesHighlight); if (this.occurrencesHighlight !== newValue) { this.occurrencesHighlight = newValue; - this._stopAll(); + switch (newValue) { + case 'off': + this._stopAll(); + break; + case 'singleFile': + this._stopAll(WordHighlighter.query?.modelInfo?.model); + break; + case 'multiFile': + if (WordHighlighter.query) { + this._run(true); + } + break; + default: + console.warn('Unknown occurrencesHighlight setting value:', newValue); + break; + } } })); @@ -423,13 +378,13 @@ class WordHighlighter { return; } - const currentDecorationIDs = WordHighlighter.storedDecorations.get(this.editor.getModel().uri); + const currentDecorationIDs = WordHighlighter.storedDecorationIDs.get(this.editor.getModel().uri); if (!currentDecorationIDs) { return; } this.editor.removeDecorations(currentDecorationIDs); - WordHighlighter.storedDecorations.delete(this.editor.getModel().uri); + WordHighlighter.storedDecorationIDs.delete(this.editor.getModel().uri); if (this.decorations.length > 0) { this.decorations.clear(); @@ -437,16 +392,16 @@ class WordHighlighter { } } - private _removeAllDecorations(): void { + private _removeAllDecorations(preservedModel?: ITextModel): void { const currentEditors = this.codeEditorService.listCodeEditors(); const deleteURI = []; // iterate over editors and store models in currentModels for (const editor of currentEditors) { - if (!editor.hasModel()) { + if (!editor.hasModel() || isEqual(editor.getModel().uri, preservedModel?.uri)) { continue; } - const currentDecorationIDs = WordHighlighter.storedDecorations.get(editor.getModel().uri); + const currentDecorationIDs = WordHighlighter.storedDecorationIDs.get(editor.getModel().uri); if (!currentDecorationIDs) { continue; } @@ -467,7 +422,7 @@ class WordHighlighter { } for (const uri of deleteURI) { - WordHighlighter.storedDecorations.delete(uri); + WordHighlighter.storedDecorationIDs.delete(uri); } } @@ -505,11 +460,11 @@ class WordHighlighter { } } - private _stopAll() { + private _stopAll(preservedModel?: ITextModel): void { // Remove any existing decorations // TODO: @Yoyokrazy -- this triggers as notebooks scroll, causing highlights to disappear momentarily. // maybe a nb type check? - this._removeAllDecorations(); + this._removeAllDecorations(preservedModel); // Cancel any renderDecorationsTimer if (this.renderDecorationsTimer !== -1) { @@ -623,13 +578,14 @@ class WordHighlighter { return currentModels; } - private _run(): void { + private _run(multiFileConfigChange?: boolean): void { let workerRequestIsValid; const hasTextFocus = this.editor.hasTextFocus(); if (!hasTextFocus) { // new nb cell scrolled in, didChangeModel fires if (!WordHighlighter.query) { // no previous query, nothing to highlight off of + this._stopAll(); return; } } else { // has text focus @@ -689,10 +645,22 @@ class WordHighlighter { this.renderDecorationsTimer = -1; this._beginRenderDecorations(); } - } else { + } else if (isEqual(this.editor.getModel().uri, WordHighlighter.query.modelInfo?.model.uri)) { // only trigger new worker requests from the primary model that initiated the query // case d) - // Stop all previous actions and start fresh - this._stopAll(); + + // check if the new queried word is contained in the range of a stored decoration for this model + if (!multiFileConfigChange) { + const currentModelDecorationRanges = this.decorations.getRanges(); + for (const storedRange of currentModelDecorationRanges) { + if (storedRange.containsPosition(this.editor.getPosition())) { + return; + } + } + } + + // stop all previous actions if new word is highlighted + // if we trigger the run off a setting change -> multifile highlighting, we do not want to remove decorations from this model + this._stopAll(multiFileConfigChange ? this.model : undefined); const myRequestId = ++this.workerRequestTokenId; this.workerRequestCompleted = false; @@ -703,7 +671,7 @@ class WordHighlighter { // 1) we have text focus, and a valid query was updated. // 2) we do not have text focus, and a valid query is cached. // the query will ALWAYS have the correct data for the current highlight request, so it can always be passed to the workerRequest safely - if (!WordHighlighter.query.modelInfo || WordHighlighter.query.modelInfo.model.isDisposed()) { + if (!WordHighlighter.query || !WordHighlighter.query.modelInfo || WordHighlighter.query.modelInfo.model.isDisposed()) { return; } this.workerRequest = this.computeWithModel(WordHighlighter.query.modelInfo.model, WordHighlighter.query.modelInfo.selection, WordHighlighter.query.word, otherModelsToHighlight); @@ -757,7 +725,7 @@ class WordHighlighter { const newDecorations: IModelDeltaDecoration[] = []; const uri = editor.getModel()?.uri; if (uri && this.workerRequestValue.has(uri)) { - const oldDecorationIDs: string[] | undefined = WordHighlighter.storedDecorations.get(uri); + const oldDecorationIDs: string[] | undefined = WordHighlighter.storedDecorationIDs.get(uri); const newDocumentHighlights = this.workerRequestValue.get(uri); if (newDocumentHighlights) { for (const highlight of newDocumentHighlights) { @@ -775,7 +743,7 @@ class WordHighlighter { editor.changeDecorations((changeAccessor) => { newDecorationIDs = changeAccessor.deltaDecorations(oldDecorationIDs ?? [], newDecorations); }); - WordHighlighter.storedDecorations = WordHighlighter.storedDecorations.set(uri, newDecorationIDs); + WordHighlighter.storedDecorationIDs = WordHighlighter.storedDecorationIDs.set(uri, newDecorationIDs); if (newDecorations.length > 0) { editorHighlighterContrib.wordHighlighter?.decorations.set(newDecorations); @@ -942,4 +910,4 @@ registerEditorContribution(WordHighlighterContribution.ID, WordHighlighterContri registerEditorAction(NextWordHighlightAction); registerEditorAction(PrevWordHighlightAction); registerEditorAction(TriggerWordHighlightAction); -// registerEditorFeature(TextualMultiDocumentHighlightFeature); +registerEditorFeature(TextualMultiDocumentHighlightFeature); diff --git a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts index 388d42021d2..29381ecf63f 100644 --- a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts @@ -48,10 +48,10 @@ export abstract class MoveWordCommand extends EditorCommand { const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators), editor.getOption(EditorOption.wordSegmenterLocales)); const model = editor.getModel(); const selections = editor.getSelections(); - + const hasMulticursor = selections.length > 1; const result = selections.map((sel) => { const inPosition = new Position(sel.positionLineNumber, sel.positionColumn); - const outPosition = this._move(wordSeparators, model, inPosition, this._wordNavigationType); + const outPosition = this._move(wordSeparators, model, inPosition, this._wordNavigationType, hasMulticursor); return this._moveTo(sel, outPosition, this._inSelectionMode); }); @@ -83,17 +83,17 @@ export abstract class MoveWordCommand extends EditorCommand { } } - protected abstract _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position; + protected abstract _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position; } export class WordLeftCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType); + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType, hasMulticursor); } } export class WordRightCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { return WordOperations.moveWordRight(wordSeparators, model, position, wordNavigationType); } } @@ -187,8 +187,8 @@ export class CursorWordAccessibilityLeft extends WordLeftCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -202,8 +202,8 @@ export class CursorWordAccessibilityLeftSelect extends WordLeftCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -295,8 +295,8 @@ export class CursorWordAccessibilityRight extends WordRightCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -310,8 +310,8 @@ export class CursorWordAccessibilityRightSelect extends WordRightCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } diff --git a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts index a06bf07200a..d90d44fd2d6 100644 --- a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isFirefox } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -179,7 +180,11 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); - test('cursorWordLeft - Recognize words', () => { + test('cursorWordLeft - Recognize words', function () { + if (isFirefox) { + // https://github.com/microsoft/vscode/issues/219843 + return this.skip(); + } const EXPECTED = [ '|/* |これ|は|テスト|です |/*', ].join('\n'); @@ -217,6 +222,40 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); + test('cursorWordLeft - issue #169904: cursors out of sync', () => { + const text = [ + '.grid1 {', + ' display: grid;', + ' grid-template-columns:', + ' [full-start] minmax(1em, 1fr)', + ' [main-start] minmax(0, 40em) [main-end]', + ' minmax(1em, 1fr) [full-end];', + '}', + '.grid2 {', + ' display: grid;', + ' grid-template-columns:', + ' [full-start] minmax(1em, 1fr)', + ' [main-start] minmax(0, 40em) [main-end] minmax(1em, 1fr) [full-end];', + '}', + ]; + withTestCodeEditor(text, {}, (editor) => { + editor.setSelections([ + new Selection(5, 44, 5, 44), + new Selection(6, 32, 6, 32), + new Selection(12, 44, 12, 44), + new Selection(12, 72, 12, 72), + ]); + cursorWordLeft(editor, false); + assert.deepStrictEqual(editor.getSelections(), [ + new Selection(5, 43, 5, 43), + new Selection(6, 31, 6, 31), + new Selection(12, 43, 12, 43), + new Selection(12, 71, 12, 71), + ]); + + }); + }); + test('cursorWordLeftSelect - issue #74369: cursorWordLeft and cursorWordLeftSelect do not behave consistently', () => { const EXPECTED = [ '|this.|is.|a.|test', @@ -365,7 +404,11 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); - test('cursorWordRight - Recognize words', () => { + test('cursorWordRight - Recognize words', function () { + if (isFirefox) { + // https://github.com/microsoft/vscode/issues/219843 + return this.skip(); + } const EXPECTED = [ '/*| これ|は|テスト|です|/*|', ].join('\n'); diff --git a/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts b/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts index ebcabc415a4..5ed4d310941 100644 --- a/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts +++ b/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts @@ -68,8 +68,8 @@ export class DeleteWordPartRight extends DeleteWordCommand { } export class WordPartLeftCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return WordPartOperations.moveWordPartLeft(wordSeparators, model, position); + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return WordPartOperations.moveWordPartLeft(wordSeparators, model, position, hasMulticursor); } } export class CursorWordPartLeft extends WordPartLeftCommand { @@ -111,7 +111,7 @@ export class CursorWordPartLeftSelect extends WordPartLeftCommand { CommandsRegistry.registerCommandAlias('cursorWordPartStartLeftSelect', 'cursorWordPartLeftSelect'); export class WordPartRightCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { return WordPartOperations.moveWordPartRight(wordSeparators, model, position); } } diff --git a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts index 12258f71cfb..b3a72759a04 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand } from 'vs/editor/browser/editorExtensions'; diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index a84a6bdcb3f..e40c7056aa7 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -31,7 +31,7 @@ import 'vs/editor/contrib/inlineProgress/browser/inlineProgress'; import 'vs/editor/contrib/gotoSymbol/browser/goToCommands'; import 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; import 'vs/editor/contrib/gotoError/browser/gotoError'; -import 'vs/editor/contrib/hover/browser/hover'; +import 'vs/editor/contrib/hover/browser/hoverContribution'; import 'vs/editor/contrib/indentation/browser/indentation'; import 'vs/editor/contrib/inlayHints/browser/inlayHintsContribution'; import 'vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace'; @@ -42,7 +42,9 @@ import 'vs/editor/contrib/links/browser/links'; import 'vs/editor/contrib/longLinesHelper/browser/longLinesHelper'; import 'vs/editor/contrib/multicursor/browser/multicursor'; import 'vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution'; +import 'vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution'; import 'vs/editor/contrib/parameterHints/browser/parameterHints'; +import 'vs/editor/contrib/placeholderText/browser/placeholderText.contribution'; import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/sectionHeaders/browser/sectionHeaders'; import 'vs/editor/contrib/semanticTokens/browser/documentSemanticTokens'; diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 709cb064db0..9700eed5a97 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -9,7 +9,7 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPosit import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IQuickInputService, IQuickInputButton, IQuickPickItem, IQuickPick, IInputBox, IQuickNavigateConfiguration, IPickOptions, QuickPickInput, IInputOptions, IQuickWidget } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, IQuickPick, IInputBox, IQuickNavigateConfiguration, IPickOptions, QuickPickInput, IInputOptions, IQuickWidget } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -18,7 +18,6 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { QuickInputController, IQuickInputControllerHost } from 'vs/platform/quickinput/browser/quickInputController'; import { QuickInputService } from 'vs/platform/quickinput/browser/quickInputService'; import { createSingleCallFunction } from 'vs/base/common/functional'; -import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; class EditorScopedQuickInputService extends QuickInputService { @@ -100,10 +99,9 @@ export class StandaloneQuickInputService implements IQuickInputService { return quickInputService; } - get quickAccess(): IQuickAccessController { return this.activeService.quickAccess; } - - get backButton(): IQuickInputButton { return this.activeService.backButton; } - + get currentQuickInput() { return this.activeService.currentQuickInput; } + get quickAccess() { return this.activeService.quickAccess; } + get backButton() { return this.activeService.backButton; } get onShow() { return this.activeService.onShow; } get onHide() { return this.activeService.onHide; } @@ -113,7 +111,7 @@ export class StandaloneQuickInputService implements IQuickInputService { ) { } - pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { + pick>(picks: Promise[]> | QuickPickInput[], options?: O, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { return (this.activeService as unknown as QuickInputController /* TS fail */).pick(picks, options, token); } @@ -121,8 +119,10 @@ export class StandaloneQuickInputService implements IQuickInputService { return this.activeService.input(options, token); } - createQuickPick(): IQuickPick { - return this.activeService.createQuickPick(); + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; + createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { + return this.activeService.createQuickPick(options); } createInputBox(): IInputBox { diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index c80bdf63686..3ccfa128dc2 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -40,7 +40,8 @@ import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditor import { IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { mainWindow } from 'vs/base/browser/window'; import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { setBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; /** * Description of an action contribution @@ -272,6 +273,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, @IKeybindingService keybindingService: IKeybindingService, @IThemeService themeService: IThemeService, @INotificationService notificationService: INotificationService, @@ -281,7 +283,6 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon ) { const options = { ..._options }; options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; - options.ariaLabel = options.ariaLabel + ';' + (StandaloneCodeEditorNLS.accessibilityHelpMessage); super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); if (keybindingService instanceof StandaloneKeybindingService) { @@ -293,6 +294,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon createAriaDomNode(options.ariaContainerElement); setHoverDelegateFactory((placement, enableInstantHover) => instantiationService.createInstance(WorkbenchHoverDelegate, placement, enableInstantHover, {})); + setBaseLayerHoverDelegate(hoverService); } public addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null { @@ -415,6 +417,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, @IKeybindingService keybindingService: IKeybindingService, @IStandaloneThemeService themeService: IStandaloneThemeService, @INotificationService notificationService: INotificationService, @@ -436,7 +439,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon } const _model: ITextModel | null | undefined = options.model; delete options.model; - super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, keybindingService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); + super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, hoverService, keybindingService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); this._configurationService = configurationService; this._standaloneThemeService = themeService; diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 5ade938e7c2..c91190d439c 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -8,23 +8,23 @@ import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import * as model from 'vs/editor/common/model'; +import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import * as languages from 'vs/editor/common/languages'; +import { ILanguageExtensionPoint, ILanguageService } from 'vs/editor/common/languages/language'; import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageExtensionPoint, ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageSelector } from 'vs/editor/common/languageSelector'; +import * as model from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums'; import { StandaloneServices } from 'vs/editor/standalone/browser/standaloneServices'; import { compile } from 'vs/editor/standalone/common/monarch/monarchCompile'; import { MonarchTokenizer } from 'vs/editor/standalone/common/monarch/monarchLexer'; import { IMonarchLanguage } from 'vs/editor/standalone/common/monarch/monarchTypes'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneTheme'; -import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { LanguageSelector } from 'vs/editor/common/languageSelector'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; +import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; /** * Register information about a new language. @@ -481,10 +481,10 @@ export function registerSignatureHelpProvider(languageSelector: LanguageSelector export function registerHoverProvider(languageSelector: LanguageSelector, provider: languages.HoverProvider): IDisposable { const languageFeaturesService = StandaloneServices.get(ILanguageFeaturesService); return languageFeaturesService.hoverProvider.register(languageSelector, { - provideHover: (model: model.ITextModel, position: Position, token: CancellationToken): Promise => { + provideHover: async (model: model.ITextModel, position: Position, token: CancellationToken, context?: languages.HoverContext): Promise => { const word = model.getWordAtPosition(position); - return Promise.resolve(provider.provideHover(model, position, token)).then((value): languages.Hover | undefined => { + return Promise.resolve(provider.provideHover(model, position, token, context)).then((value): languages.Hover | undefined => { if (!value) { return undefined; } @@ -809,7 +809,9 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { InlineEditTriggerKind: standaloneEnums.InlineEditTriggerKind, CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, NewSymbolNameTag: standaloneEnums.NewSymbolNameTag, + NewSymbolNameTriggerKind: standaloneEnums.NewSymbolNameTriggerKind, PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, + HoverVerbosityAction: standaloneEnums.HoverVerbosityAction, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index c3d2f3c41c3..9682a3cfb0f 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -15,7 +15,7 @@ import 'vs/editor/browser/services/hoverService/hoverService'; import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event, IValueWithChangeEvent, ValueWithChangeEvent } from 'vs/base/common/event'; import { ResolvedKeybinding, KeyCodeChord, Keybinding, decodeKeybinding } from 'vs/base/common/keybindings'; import { IDisposable, IReference, ImmortalReference, toDisposable, DisposableStore, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { OS, isLinux, isMacintosh } from 'vs/base/common/platform'; @@ -55,7 +55,7 @@ import { ConsoleLogger, ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceTrustManagementService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { IContextMenuService, IContextViewDelegate, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewDelegate, IContextViewService, IOpenContextView } from 'vs/platform/contextview/browser/contextView'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { LanguageService } from 'vs/editor/common/services/languageService'; import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuService'; @@ -88,12 +88,13 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { DefaultConfiguration } from 'vs/platform/configuration/common/configurations'; import { WorkspaceEdit } from 'vs/editor/common/languages'; -import { AccessibilitySignal, IAccessibilitySignalService, Sound } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { AccessibilitySignal, AccessibilityModality, IAccessibilitySignalService, Sound } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { LogService } from 'vs/platform/log/common/logService'; import { getEditorFeatures } from 'vs/editor/common/editorFeatures'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionKind, IEnvironmentService, IExtensionHostDebugParams } from 'vs/platform/environment/common/environment'; import { mainWindow } from 'vs/base/browser/window'; +import { ResourceMap } from 'vs/base/common/map'; class SimpleModel implements IResolvedTextEditorModel { @@ -250,7 +251,7 @@ class StandaloneDialogService implements IDialogService { return { confirmed, checkboxChecked: false // unsupported - } as IConfirmationResult; + }; } private doConfirm(message: string, detail?: string): boolean { @@ -629,9 +630,22 @@ export class StandaloneConfigurationService implements IConfigurationService { private readonly _configuration: Configuration; - constructor() { - const defaultConfiguration = new DefaultConfiguration(); - this._configuration = new Configuration(defaultConfiguration.reload(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + constructor( + @ILogService private readonly logService: ILogService, + ) { + const defaultConfiguration = new DefaultConfiguration(logService); + this._configuration = new Configuration( + defaultConfiguration.reload(), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ResourceMap(), + ConfigurationModel.createEmptyModel(logService), + new ResourceMap(), + logService + ); defaultConfiguration.dispose(); } @@ -660,7 +674,7 @@ export class StandaloneConfigurationService implements IConfigurationService { } if (changedKeys.length > 0) { - const configurationChangeEvent = new ConfigurationChangeEvent({ keys: changedKeys, overrides: [] }, previous, this._configuration); + const configurationChangeEvent = new ConfigurationChangeEvent({ keys: changedKeys, overrides: [] }, previous, this._configuration, undefined, this.logService); configurationChangeEvent.source = ConfigurationTarget.MEMORY; this._onDidChangeConfiguration.fire(configurationChangeEvent); } @@ -778,6 +792,7 @@ class StandaloneTelemetryService implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; + readonly devDeviceId = 'someValue.devDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; setEnabled(): void { } @@ -975,7 +990,7 @@ class StandaloneContextViewService extends ContextViewService { super(layoutService); } - override showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable { + override showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IOpenContextView { if (!container) { const codeEditor = this._codeEditorService.getFocusedCodeEditor() || this._codeEditorService.getActiveCodeEditor(); if (codeEditor) { @@ -1065,6 +1080,14 @@ class StandaloneAccessbilitySignalService implements IAccessibilitySignalService async playSignals(cues: AccessibilitySignal[]): Promise { } + getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent { + return ValueWithChangeEvent.const(false); + } + + getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number { + return 0; + } + isSoundEnabled(cue: AccessibilitySignal): boolean { return false; } @@ -1077,10 +1100,6 @@ class StandaloneAccessbilitySignalService implements IAccessibilitySignalService return Event.None; } - onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event { - return Event.None; - } - async playSound(cue: Sound, allowManyInParallel?: boolean | undefined): Promise { } playSignalLoop(cue: AccessibilitySignal): IDisposable { @@ -1092,6 +1111,7 @@ export interface IEditorOverrideServices { [index: string]: any; } +registerSingleton(ILogService, StandaloneLogService, InstantiationType.Eager); registerSingleton(IConfigurationService, StandaloneConfigurationService, InstantiationType.Eager); registerSingleton(ITextResourceConfigurationService, StandaloneResourceConfigurationService, InstantiationType.Eager); registerSingleton(ITextResourcePropertiesService, StandaloneResourcePropertiesService, InstantiationType.Eager); @@ -1104,7 +1124,6 @@ registerSingleton(INotificationService, StandaloneNotificationService, Instantia registerSingleton(IMarkerService, MarkerService, InstantiationType.Eager); registerSingleton(ILanguageService, StandaloneLanguageService, InstantiationType.Eager); registerSingleton(IStandaloneThemeService, StandaloneThemeService, InstantiationType.Eager); -registerSingleton(ILogService, StandaloneLogService, InstantiationType.Eager); registerSingleton(IModelService, ModelService, InstantiationType.Eager); registerSingleton(IMarkerDecorationsService, MarkerDecorationsService, InstantiationType.Eager); registerSingleton(IContextKeyService, ContextKeyService, InstantiationType.Eager); diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index e9f5f3934c4..599c89a0213 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -434,21 +434,21 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm } // Create our lexer - const lexer: monarchCommon.ILexer = {}; - lexer.languageId = languageId; - lexer.includeLF = bool(json.includeLF, false); - lexer.noThrow = false; // raise exceptions during compilation - lexer.maxStack = 100; - - // Set standard fields: be defensive about types - lexer.start = (typeof json.start === 'string' ? json.start : null); - lexer.ignoreCase = bool(json.ignoreCase, false); - lexer.unicode = bool(json.unicode, false); - - lexer.tokenPostfix = string(json.tokenPostfix, '.' + lexer.languageId); - lexer.defaultToken = string(json.defaultToken, 'source'); - - lexer.usesEmbedded = false; // becomes true if we find a nextEmbedded action + const lexer: monarchCommon.ILexer = { + languageId: languageId, + includeLF: bool(json.includeLF, false), + noThrow: false, // raise exceptions during compilation + maxStack: 100, + start: (typeof json.start === 'string' ? json.start : null), + ignoreCase: bool(json.ignoreCase, false), + unicode: bool(json.unicode, false), + tokenPostfix: string(json.tokenPostfix, '.' + languageId), + defaultToken: string(json.defaultToken, 'source'), + usesEmbedded: false, // becomes true if we find a nextEmbedded action + stateNames: {}, + tokenizer: {}, + brackets: [] + }; // For calling compileAction later on const lexerMin: monarchCommon.ILexerMin = json; diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index fd55718ca7d..7a1ba1d9d75 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Token, TokenizationRegistry } from 'vs/editor/common/languages'; @@ -14,6 +14,7 @@ import { compile } from 'vs/editor/standalone/common/monarch/monarchCompile'; import { MonarchTokenizer } from 'vs/editor/standalone/common/monarch/monarchLexer'; import { IMonarchLanguage } from 'vs/editor/standalone/common/monarch/monarchTypes'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NullLogService } from 'vs/platform/log/common/log'; suite('Monarch', () => { @@ -37,7 +38,7 @@ suite('Monarch', () => { test('Ensure @rematch and nextEmbedded can be used together in Monarch grammar', () => { const disposables = new DisposableStore(); const languageService = disposables.add(new LanguageService()); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); disposables.add(languageService.registerLanguage({ id: 'sql' })); disposables.add(TokenizationRegistry.register('sql', disposables.add(createMonarchTokenizer(languageService, 'sql', { tokenizer: { @@ -113,7 +114,7 @@ suite('Monarch', () => { test('microsoft/monaco-editor#1235: Empty Line Handling', () => { const disposables = new DisposableStore(); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); const languageService = disposables.add(new LanguageService()); const tokenizer = disposables.add(createMonarchTokenizer(languageService, 'test', { tokenizer: { @@ -172,7 +173,7 @@ suite('Monarch', () => { test('microsoft/monaco-editor#2265: Exit a state at end of line', () => { const disposables = new DisposableStore(); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); const languageService = disposables.add(new LanguageService()); const tokenizer = disposables.add(createMonarchTokenizer(languageService, 'test', { includeLF: true, @@ -223,7 +224,7 @@ suite('Monarch', () => { test('issue #115662: monarchCompile function need an extra option which can control replacement', () => { const disposables = new DisposableStore(); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); const languageService = disposables.add(new LanguageService()); const tokenizer1 = disposables.add(createMonarchTokenizer(languageService, 'test', { @@ -232,7 +233,7 @@ suite('Monarch', () => { uselessReplaceKey2: '@uselessReplaceKey3', uselessReplaceKey3: '@uselessReplaceKey4', uselessReplaceKey4: '@uselessReplaceKey5', - uselessReplaceKey5: '@ham' || '', + uselessReplaceKey5: '@ham', tokenizer: { root: [ { @@ -280,7 +281,7 @@ suite('Monarch', () => { test('microsoft/monaco-editor#2424: Allow to target @@', () => { const disposables = new DisposableStore(); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); const languageService = disposables.add(new LanguageService()); const tokenizer = disposables.add(createMonarchTokenizer(languageService, 'test', { @@ -312,7 +313,7 @@ suite('Monarch', () => { test('microsoft/monaco-editor#3025: Check maxTokenizationLineLength before tokenizing', async () => { const disposables = new DisposableStore(); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); const languageService = disposables.add(new LanguageService()); // Set maxTokenizationLineLength to 4 so that "ham" works but "hamham" would fail @@ -348,7 +349,7 @@ suite('Monarch', () => { test('microsoft/monaco-editor#3128: allow state access within rules', () => { const disposables = new DisposableStore(); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); const languageService = disposables.add(new LanguageService()); const tokenizer = disposables.add(createMonarchTokenizer(languageService, 'test', { diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 92e385564ac..745c0075541 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/standalone/test/browser/standaloneServices.test.ts b/src/vs/editor/standalone/test/browser/standaloneServices.test.ts index e716ea922d5..649783848e3 100644 --- a/src/vs/editor/standalone/test/browser/standaloneServices.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneServices.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -32,7 +32,7 @@ suite('StandaloneKeybindingService', () => { const disposables = new DisposableStore(); const serviceCollection = new ServiceCollection(); const instantiationService = new InstantiationService(serviceCollection, true); - const configurationService = new StandaloneConfigurationService(); + const configurationService = new StandaloneConfigurationService(new NullLogService()); const contextKeyService = disposables.add(new ContextKeyService(configurationService)); const commandService = new StandaloneCommandService(instantiationService); const notificationService = new StandaloneNotificationService(); diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index a769b21c4f9..8d98e803658 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index 6cdb2657aa7..fbd9a36c95a 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index 5b4d0994a62..15f9bb999d4 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TrimTrailingWhitespaceCommand, trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; diff --git a/src/vs/editor/test/browser/config/editorConfiguration.test.ts b/src/vs/editor/test/browser/config/editorConfiguration.test.ts index b507c54686f..911b73018c3 100644 --- a/src/vs/editor/test/browser/config/editorConfiguration.test.ts +++ b/src/vs/editor/test/browser/config/editorConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvConfiguration } from 'vs/editor/browser/config/editorConfiguration'; import { migrateOptions } from 'vs/editor/browser/config/migrateOptions'; diff --git a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index 4f644203ef4..a2e3ace58f6 100644 --- a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ComputedEditorOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorLayoutInfo, EditorLayoutInfoComputer, EditorMinimapOptions, EditorOption, EditorOptions, InternalEditorRenderLineNumbersOptions, InternalEditorScrollbarOptions, RenderLineNumbersType, RenderMinimap } from 'vs/editor/common/config/editorOptions'; @@ -60,7 +60,8 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { scale: 1, showRegionSectionHeaders: true, showMarkSectionHeaders: true, - sectionHeaderFontSize: 9 + sectionHeaderFontSize: 9, + sectionHeaderLetterSpacing: 1, }; options._write(EditorOption.minimap, minimapOptions); const scrollbarOptions: InternalEditorScrollbarOptions = { diff --git a/src/vs/editor/test/browser/config/testConfiguration.ts b/src/vs/editor/test/browser/config/testConfiguration.ts index b5d42908ded..4a2e87e6d48 100644 --- a/src/vs/editor/test/browser/config/testConfiguration.ts +++ b/src/vs/editor/test/browser/config/testConfiguration.ts @@ -9,11 +9,12 @@ import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; import { TestCodeEditorCreationOptions } from 'vs/editor/test/browser/testCodeEditor'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; +import { MenuId } from 'vs/platform/actions/common/actions'; export class TestConfiguration extends EditorConfiguration { constructor(opts: Readonly) { - super(false, opts, null, new TestAccessibilityService()); + super(false, MenuId.EditorContext, opts, null, new TestAccessibilityService()); } protected override _readEnvConfiguration(): IEnvConfiguration { diff --git a/src/vs/editor/test/browser/controller/cursor.integrationTest.ts b/src/vs/editor/test/browser/controller/cursor.integrationTest.ts index 6373a6fd603..bd956a35398 100644 --- a/src/vs/editor/test/browser/controller/cursor.integrationTest.ts +++ b/src/vs/editor/test/browser/controller/cursor.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Selection } from 'vs/editor/common/core/selection'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index a86e95c1303..bb2abffb9c9 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 2e312c53e3e..c4bee4b9f2b 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts index 19fcf3b1970..fd2e23e3751 100644 --- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { OperatingSystem } from 'vs/base/common/platform'; diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index 087e99311f2..2baa9ddda39 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ITextAreaWrapper, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index b8eb27e6d0f..9160c07070a 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 26dea91e681..d732776a800 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/services/testTreeSitterService.ts b/src/vs/editor/test/browser/services/testTreeSitterService.ts new file mode 100644 index 00000000000..f449962d6cd --- /dev/null +++ b/src/vs/editor/test/browser/services/testTreeSitterService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppResourcePath } from 'vs/base/common/network'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITreeSitterParserService } from 'vs/editor/common/services/treeSitterParserService'; + +export class TestTreeSitterParserService implements ITreeSitterParserService { + getLanguage(model: ITextModel): Parser.Language | undefined { + throw new Error('Method not implemented.'); + } + getLanguageLocation(languageId: string): AppResourcePath { + throw new Error('Method not implemented.'); + } + readonly _serviceBrand: undefined; + + public initTreeSitter(): Promise { + return Promise.resolve(); + } + + public getTree(_model: ITextModel): Parser.Tree | undefined { + return undefined; + } +} diff --git a/src/vs/editor/test/browser/services/treeSitterParserService.test.ts b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts new file mode 100644 index 00000000000..be1b58185ad --- /dev/null +++ b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/test/common/utils'; +import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from 'vs/editor/browser/services/treeSitter/treeSitterParserService'; +import type { Parser } from '@vscode/tree-sitter-wasm'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { timeout } from 'vs/base/common/async'; +import { ConsoleMainLogger, ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { LogService } from 'vs/platform/log/common/logService'; +import { mock } from 'vs/base/test/common/mock'; + +class MockParser implements Parser { + static async init(): Promise { } + delete(): void { } + parse(input: string | Parser.Input, oldTree?: Parser.Tree, options?: Parser.Options): Parser.Tree { + return new MockTree(); + } + getIncludedRanges(): Parser.Range[] { + return []; + } + getTimeoutMicros(): number { return 0; } + setTimeoutMicros(timeout: number): void { } + reset(): void { } + getLanguage(): Parser.Language { return {} as any; } + setLanguage(): void { } + getLogger(): Parser.Logger { + throw new Error('Method not implemented.'); + } + setLogger(logFunc?: Parser.Logger | false | null): void { + throw new Error('Method not implemented.'); + } +} + +class MockTreeSitterImporter extends TreeSitterImporter { + public override async getParserClass(): Promise { + return MockParser as any; + } +} + +class MockTree implements Parser.Tree { + editorLanguage: string = ''; + editorContents: string = ''; + rootNode: Parser.SyntaxNode = {} as any; + rootNodeWithOffset(offsetBytes: number, offsetExtent: Parser.Point): Parser.SyntaxNode { + throw new Error('Method not implemented.'); + } + copy(): Parser.Tree { + throw new Error('Method not implemented.'); + } + delete(): void { } + edit(edit: Parser.Edit): Parser.Tree { + return this; + } + walk(): Parser.TreeCursor { + throw new Error('Method not implemented.'); + } + getChangedRanges(other: Parser.Tree): Parser.Range[] { + throw new Error('Method not implemented.'); + } + getIncludedRanges(): Parser.Range[] { + throw new Error('Method not implemented.'); + } + getEditedRange(other: Parser.Tree): Parser.Range { + throw new Error('Method not implemented.'); + } + getLanguage(): Parser.Language { + throw new Error('Method not implemented.'); + } +} + +class MockLanguage implements Parser.Language { + version: number = 0; + fieldCount: number = 0; + stateCount: number = 0; + nodeTypeCount: number = 0; + fieldNameForId(fieldId: number): string | null { + throw new Error('Method not implemented.'); + } + fieldIdForName(fieldName: string): number | null { + throw new Error('Method not implemented.'); + } + idForNodeType(type: string, named: boolean): number { + throw new Error('Method not implemented.'); + } + nodeTypeForId(typeId: number): string | null { + throw new Error('Method not implemented.'); + } + nodeTypeIsNamed(typeId: number): boolean { + throw new Error('Method not implemented.'); + } + nodeTypeIsVisible(typeId: number): boolean { + throw new Error('Method not implemented.'); + } + nextState(stateId: number, typeId: number): number { + throw new Error('Method not implemented.'); + } + query(source: string): Parser.Query { + throw new Error('Method not implemented.'); + } + lookaheadIterator(stateId: number): Parser.LookaheadIterable | null { + throw new Error('Method not implemented.'); + } + languageId: string = ''; +} + +suite('TreeSitterParserService', function () { + const treeSitterImporter: TreeSitterImporter = new MockTreeSitterImporter(); + let logService: ILogService; + let telemetryService: ITelemetryService; + setup(function () { + logService = new LogService(new ConsoleMainLogger()); + telemetryService = new class extends mock() { + override async publicLog2() { + // + } + }; + }); + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('TextModelTreeSitter race condition: first language is slow to load', async function () { + class MockTreeSitterParser extends TreeSitterLanguages { + public override async getLanguage(languageId: string): Promise { + if (languageId === 'javascript') { + await timeout(200); + } + const language = new MockLanguage(); + language.languageId = languageId; + return language; + } + } + + const treeSitterParser: TreeSitterLanguages = store.add(new MockTreeSitterParser(treeSitterImporter, {} as any)); + const textModel = store.add(createTextModel('console.log("Hello, world!");', 'javascript')); + const textModelTreeSitter = store.add(new TextModelTreeSitter(textModel, treeSitterParser, treeSitterImporter, logService, telemetryService)); + textModel.setLanguage('typescript'); + await timeout(300); + assert.strictEqual((textModelTreeSitter.tree?.language as MockLanguage).languageId, 'typescript'); + }); +}); diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index c0a4b9d1cf3..5fcd0c1bd4c 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -32,6 +32,7 @@ import { TestTextResourcePropertiesService } from 'vs/editor/test/common/service import { instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { TestClipboardService } from 'vs/platform/clipboard/test/common/testClipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -68,7 +69,7 @@ export interface ITestCodeEditor extends IActiveCodeEditor { export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { //#region testing overrides - protected override _createConfiguration(isSimpleWidget: boolean, options: Readonly): EditorConfiguration { + protected override _createConfiguration(isSimpleWidget: boolean, contextMenuId: MenuId, options: Readonly): EditorConfiguration { return new TestConfiguration(options); } protected override _createView(viewModel: ViewModel): [View, boolean] { diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 60abb6d301f..e12d2cc526a 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IRange } from 'vs/editor/common/core/range'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; import { ICommand, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index ab0d682a7be..4fcfa5cf3cd 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory'; import { Constants } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet'; diff --git a/src/vs/editor/test/browser/view/viewLayer.test.ts b/src/vs/editor/test/browser/view/viewLayer.test.ts index 0fcb0b1d57d..0f9588d870c 100644 --- a/src/vs/editor/test/browser/view/viewLayer.test.ts +++ b/src/vs/editor/test/browser/view/viewLayer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILine, RenderedLinesCollection } from 'vs/editor/browser/view/viewLayer'; diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 0974ab8d183..292aa0ed1fa 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; diff --git a/src/vs/editor/test/browser/viewModel/testViewModel.ts b/src/vs/editor/test/browser/viewModel/testViewModel.ts index fe7f0c6e2db..36749b71bd6 100644 --- a/src/vs/editor/test/browser/viewModel/testViewModel.ts +++ b/src/vs/editor/test/browser/viewModel/testViewModel.ts @@ -22,6 +22,8 @@ export function testViewModel(text: string[], options: IEditorOptions, callback: const viewModel = new ViewModel(EDITOR_ID, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!, testLanguageConfigurationService, new TestThemeService(), { setVisibleLines(visibleLines, stabilized) { }, + }, { + batchChanges: (cb) => cb(), }); callback(viewModel, model); diff --git a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts index 02786c056b3..a89fc01e245 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts index 4b515f974b2..ff16b570be1 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts index 192b4b6d8e4..a09d5c98f18 100644 --- a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts b/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts index 1f08e089c2e..53ecda34e13 100644 --- a/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { UnchangedRegion } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { LineRange } from 'vs/editor/common/core/lineRange'; diff --git a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts new file mode 100644 index 00000000000..0a104966470 --- /dev/null +++ b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -0,0 +1,226 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DisposableStore } from "vs/base/common/lifecycle"; +import { IObservable, derivedHandleChanges } from "vs/base/common/observable"; +import { ensureNoDisposablesAreLeakedInTestSuite } from "vs/base/test/common/utils"; +import { ICodeEditor } from "vs/editor/browser/editorBrowser"; +import { ObservableCodeEditor, observableCodeEditor } from "vs/editor/browser/observableCodeEditor"; +import { Position } from "vs/editor/common/core/position"; +import { Range } from "vs/editor/common/core/range"; +import { ViewModel } from "vs/editor/common/viewModel/viewModelImpl"; +import { withTestCodeEditor } from "vs/editor/test/browser/testCodeEditor"; + +suite("CodeEditorWidget", () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function withTestFixture( + cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable }) => void + ) { + withEditorSetupTestFixture(undefined, cb); + } + + function withEditorSetupTestFixture( + preSetupCallback: + | ((editor: ICodeEditor, disposables: DisposableStore) => void) + | undefined, + cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable }) => void + ) { + withTestCodeEditor("hello world", {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + preSetupCallback?.(editor, disposables); + const obsEditor = observableCodeEditor(editor); + const log = new Log(); + + const derived = derivedHandleChanges( + { + createEmptyChangeSummary: () => undefined, + handleChange: (context) => { + const obsName = observableName(context.changedObservable, obsEditor); + log.log(`handle change: ${obsName} ${formatChange(context.change)}`); + return true; + }, + }, + (reader) => { + const versionId = obsEditor.versionId.read(reader); + const selection = obsEditor.selections.read(reader)?.map((s) => s.toString()).join(", "); + obsEditor.onDidType.read(reader); + + const str = `running derived: selection: ${selection}, value: ${versionId}`; + log.log(str); + return str; + } + ); + + derived.recomputeInitiallyAndOnChange(disposables); + assert.deepStrictEqual(log.getAndClearEntries(), [ + "running derived: selection: [1,1 -> 1,1], value: 1", + ]); + + cb({ editor, viewModel, log, derived }); + + disposables.dispose(); + }); + } + + test("setPosition", () => + withTestFixture(({ editor, log }) => { + editor.setPosition(new Position(1, 2)); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":1,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"api","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 1", + ]); + })); + + test("keyboard.type", () => + withTestFixture(({ editor, log }) => { + editor.trigger("keyboard", "type", { text: "abc" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.onDidType "abc"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,4 -> 1,4], value: 4", + ]); + })); + + test("keyboard.type and set position", () => + withTestFixture(({ editor, log }) => { + editor.trigger("keyboard", "type", { text: "abc" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.onDidType "abc"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,4 -> 1,4], value: 4", + ]); + + editor.setPosition(new Position(1, 5), "test"); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.selections {"selection":"[1,5 -> 1,5]","modelVersionId":4,"oldSelections":["[1,4 -> 1,4]"],"oldModelVersionId":4,"source":"test","reason":0}', + "running derived: selection: [1,5 -> 1,5], value: 4", + ]); + })); + + test("listener interaction (unforced)", () => { + let derived: IObservable; + let log: Log; + withEditorSetupTestFixture( + (editor, disposables) => { + disposables.add( + editor.onDidChangeModelContent(() => { + log.log(">>> before get"); + derived.get(); + log.log("<<< after get"); + }) + ); + }, + (args) => { + const editor = args.editor; + derived = args.derived; + log = args.log; + + editor.trigger("keyboard", "type", { text: "a" }); + assert.deepStrictEqual(log.getAndClearEntries(), [ + ">>> before get", + "<<< after get", + 'handle change: editor.onDidType "a"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 2", + ]); + } + ); + }); + + test("listener interaction ()", () => { + let derived: IObservable; + let log: Log; + withEditorSetupTestFixture( + (editor, disposables) => { + disposables.add( + editor.onDidChangeModelContent(() => { + log.log(">>> before forceUpdate"); + observableCodeEditor(editor).forceUpdate(); + + log.log(">>> before get"); + derived.get(); + log.log("<<< after get"); + }) + ); + }, + (args) => { + const editor = args.editor; + derived = args.derived; + log = args.log; + + editor.trigger("keyboard", "type", { text: "a" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + ">>> before forceUpdate", + ">>> before get", + "handle change: editor.versionId undefined", + "running derived: selection: [1,2 -> 1,2], value: 2", + "<<< after get", + 'handle change: editor.onDidType "a"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 2", + ]); + } + ); + }); +}); + +class Log { + private readonly entries: string[] = []; + public log(message: string): void { + this.entries.push(message); + } + + public getAndClearEntries(): string[] { + const entries = [...this.entries]; + this.entries.length = 0; + return entries; + } +} + +function formatChange(change: unknown) { + return JSON.stringify( + change, + (key, value) => { + if (value instanceof Range) { + return value.toString(); + } + if ( + value === false || + (Array.isArray(value) && value.length === 0) + ) { + return undefined; + } + return value; + } + ); +} + +function observableName(obs: IObservable, obsEditor: ObservableCodeEditor): string { + switch (obs) { + case obsEditor.selections: + return "editor.selections"; + case obsEditor.versionId: + return "editor.versionId"; + case obsEditor.onDidType: + return "editor.onDidType"; + default: + return "unknown"; + } +} diff --git a/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts b/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts index 200cb5e226a..9c2efda4f77 100644 --- a/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts +++ b/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/cursor/cursorAtomicMoveOperations'; diff --git a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts index c2d8dc85a45..e90bdd5a571 100644 --- a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts +++ b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; diff --git a/src/vs/editor/test/common/core/characterClassifier.test.ts b/src/vs/editor/test/common/core/characterClassifier.test.ts index dd4c4b02df3..4271d91f923 100644 --- a/src/vs/editor/test/common/core/characterClassifier.test.ts +++ b/src/vs/editor/test/common/core/characterClassifier.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; diff --git a/src/vs/editor/test/common/core/lineRange.test.ts b/src/vs/editor/test/common/core/lineRange.test.ts index b67b9bdfb7b..1b45b3f2829 100644 --- a/src/vs/editor/test/common/core/lineRange.test.ts +++ b/src/vs/editor/test/common/core/lineRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index 177e66774df..d2457fa2bc8 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; diff --git a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts index 39aead0a848..1811a08e90f 100644 --- a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts +++ b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; diff --git a/src/vs/editor/test/common/core/range.test.ts b/src/vs/editor/test/common/core/range.test.ts index bf574592d4e..fcbb0cd0fcc 100644 --- a/src/vs/editor/test/common/core/range.test.ts +++ b/src/vs/editor/test/common/core/range.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/core/stringBuilder.test.ts b/src/vs/editor/test/common/core/stringBuilder.test.ts index af90a1f5eb1..6afe99db33a 100644 --- a/src/vs/editor/test/common/core/stringBuilder.test.ts +++ b/src/vs/editor/test/common/core/stringBuilder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { writeUInt16LE } from 'vs/base/common/buffer'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/core/testLineToken.ts b/src/vs/editor/test/common/core/testLineToken.ts index 8217c2042fb..f3c49807941 100644 --- a/src/vs/editor/test/common/core/testLineToken.ts +++ b/src/vs/editor/test/common/core/testLineToken.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; -import { ColorId, TokenMetadata, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes'; +import { ColorId, TokenMetadata, ITokenPresentation, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageIdCodec } from 'vs/editor/common/languages'; /** * A token on a line. @@ -22,6 +23,10 @@ export class TestLineToken { this._metadata = metadata; } + public getStandardTokenType(): StandardTokenType { + return TokenMetadata.getTokenType(this._metadata); + } + public getForeground(): ColorId { return TokenMetadata.getForeground(this._metadata); } @@ -79,6 +84,10 @@ export class TestLineTokens implements IViewLineTokens { return this._actual.length; } + public getStandardTokenType(tokenIndex: number): StandardTokenType { + return this._actual[tokenIndex].getStandardTokenType(); + } + public getForeground(tokenIndex: number): ColorId { return this._actual[tokenIndex].getForeground(); } @@ -114,6 +123,18 @@ export class TestLineTokens implements IViewLineTokens { public getLanguageId(tokenIndex: number): string { throw new Error('Method not implemented.'); } + + public getTokenText(tokenIndex: number): string { + throw new Error('Method not implemented.'); + } + + public forEach(callback: (tokenIndex: number) => void): void { + throw new Error('Not implemented'); + } + + public get languageIdCodec(): ILanguageIdCodec { + throw new Error('Not implemented'); + } } export class TestLineTokenFactory { diff --git a/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts index f02e8a9bd50..4458eaf8a5b 100644 --- a/src/vs/editor/test/common/core/textEdit.test.ts +++ b/src/vs/editor/test/common/core/textEdit.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { StringText } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 38378ef3826..651dc5a79f2 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { Constants } from 'vs/base/common/uint'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts index 264292f82ff..514556238b5 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { splitLines } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts index 2a21907778d..1c2ea8c3b49 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index 9155b32a9ca..a1def431d2b 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts index 42d0eae8f81..6fced6cef3e 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AstNode, AstNodeKind, ListAstNode, TextAstNode } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast'; import { concat23Trees } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts index 09c9e34a124..87215503dc4 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore, disposeOnReturn } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts index eb49c1048d1..57086d81ff8 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Length, lengthAdd, lengthDiffNonNegative, lengthToObj, toLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts index 45d4dcc6833..2b5026e4f0c 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DenseKeyProvider, SmallImmutableSet } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index a83b30a66eb..f306d99ccab 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageId, MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/model/editStack.test.ts b/src/vs/editor/test/common/model/editStack.test.ts index d220d12e668..da409c9d8a3 100644 --- a/src/vs/editor/test/common/model/editStack.test.ts +++ b/src/vs/editor/test/common/model/editStack.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; import { TextChange } from 'vs/editor/common/core/textChange'; diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index aa39ad03e8c..d2af9519432 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index 06803315a7b..2228a6174a4 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { EndOfLinePreference, EndOfLineSequence } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index 06534b6e2a6..dd2fd332be5 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { IntervalNode, IntervalTree, NodeColor, SENTINEL, getNodeColor, intervalCompare, nodeAcceptEdit, setNodeStickiness } from 'vs/editor/common/model/intervalTree'; @@ -912,4 +912,3 @@ function assertValidTree(T: IntervalTree): void { } //#endregion - diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 77bc1e74cc1..a4604874d65 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts index 22f182090b8..662ef1fe8fd 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 5eb7462e045..91279c1d70d 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index fa48350404f..66df53d83df 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index a75e86e7b59..e699174a650 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index dda14417b1a..c00d0ce8f2a 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index a2cd24ce4f4..0aeebe90c6f 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/modelInjectedText.test.ts b/src/vs/editor/test/common/model/modelInjectedText.test.ts index 72d0f9ba0a3..f7c023c4f8a 100644 --- a/src/vs/editor/test/common/model/modelInjectedText.test.ts +++ b/src/vs/editor/test/common/model/modelInjectedText.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 007033dc790..01cc7cb6ef6 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { WordCharacterClassifier } from 'vs/editor/common/core/wordCharacterClassifier'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -1817,6 +1817,22 @@ suite('buffer api', () => { assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); + + test('getNearestChunk', () => { + const pieceTree = createTextBuffer(['012345678']); + ds.add(pieceTree); + const pt = pieceTree.getPieceTree(); + + pt.insert(3, 'ABC'); + assert.equal(pt.getLineContent(1), '012ABC345678'); + assert.equal(pt.getNearestChunk(3), 'ABC'); + assert.equal(pt.getNearestChunk(6), '345678'); + + pt.delete(9, 1); + assert.equal(pt.getLineContent(1), '012ABC34578'); + assert.equal(pt.getNearestChunk(6), '345'); + assert.equal(pt.getNearestChunk(9), '78'); + }); }); suite('search offset cache', () => { diff --git a/src/vs/editor/test/common/model/textChange.test.ts b/src/vs/editor/test/common/model/textChange.test.ts index 164e7ff92e1..a58430b309f 100644 --- a/src/vs/editor/test/common/model/textChange.test.ts +++ b/src/vs/editor/test/common/model/textChange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { compressConsecutiveTextChanges, TextChange } from 'vs/editor/common/core/textChange'; diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index dc6037d6df4..3270a563386 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { UTF8_BOM_CHARACTER } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 91ec41810f3..0f03a1e0730 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/textModelTokens.test.ts b/src/vs/editor/test/common/model/textModelTokens.test.ts index b425cc30fbf..34171ea9b1d 100644 --- a/src/vs/editor/test/common/model/textModelTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { RangePriorityQueueImpl } from 'vs/editor/common/model/textModelTokens'; diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index cdb8528819b..54ef6b8d628 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index 7ceba9ef598..f4e9413a422 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index 97c74722cc9..28e6340d9f3 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index ce3aa3f4078..3de1b762b59 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageSelector, score } from 'vs/editor/common/languageSelector'; diff --git a/src/vs/editor/test/common/modes/linkComputer.test.ts b/src/vs/editor/test/common/modes/linkComputer.test.ts index 49411db88e9..2d769837672 100644 --- a/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILink } from 'vs/editor/common/languages'; import { ILinkComputerTarget, computeLinks } from 'vs/editor/common/languages/linkComputer'; diff --git a/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts b/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts new file mode 100644 index 00000000000..0f5ebc499bd --- /dev/null +++ b/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAutoClosingPair, IAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; + +export const javascriptAutoClosingPairsRules: IAutoClosingPairConditional[] = [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] } +]; + +export const latexAutoClosingPairsRules: IAutoClosingPair[] = [ + { open: '\\left(', close: '\\right)' }, + { open: '\\left[', close: '\\right]' }, + { open: '\\left\\{', close: '\\right\\}' }, + { open: '\\bigl(', close: '\\bigr)' }, + { open: '\\bigl[', close: '\\bigr]' }, + { open: '\\bigl\\{', close: '\\bigr\\}' }, + { open: '\\Bigl(', close: '\\Bigr)' }, + { open: '\\Bigl[', close: '\\Bigr]' }, + { open: '\\Bigl\\{', close: '\\Bigr\\}' }, + { open: '\\biggl(', close: '\\biggr)' }, + { open: '\\biggl[', close: '\\biggr]' }, + { open: '\\biggl\\{', close: '\\biggr\\}' }, + { open: '\\Biggl(', close: '\\Biggr)' }, + { open: '\\Biggl[', close: '\\Biggr]' }, + { open: '\\Biggl\\{', close: '\\Biggr\\}' }, + { open: '\\(', close: '\\)' }, + { open: '\\[', close: '\\]' }, + { open: '\\{', close: '\\}' }, + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '`', close: '\'' }, +]; diff --git a/src/vs/editor/test/common/modes/supports/bracketRules.ts b/src/vs/editor/test/common/modes/supports/bracketRules.ts new file mode 100644 index 00000000000..d21b70a6dc3 --- /dev/null +++ b/src/vs/editor/test/common/modes/supports/bracketRules.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CharacterPair } from 'vs/editor/common/languages/languageConfiguration'; + +const standardBracketRules: CharacterPair[] = [ + ['{', '}'], + ['[', ']'], + ['(', ')'] +]; + +export const rubyBracketRules = standardBracketRules; + +export const cppBracketRules = standardBracketRules; + +export const goBracketRules = standardBracketRules; + +export const phpBracketRules = standardBracketRules; + +export const vbBracketRules = standardBracketRules; + +export const luaBracketRules = standardBracketRules; + +export const htmlBracketRules: CharacterPair[] = [ + [''], + ['{', '}'], + ['(', ')'] +]; + +export const typescriptBracketRules: CharacterPair[] = [ + ['${', '}'], + ['{', '}'], + ['[', ']'], + ['(', ')'] +]; + +export const latexBracketRules: CharacterPair[] = [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ['[', ')'], + ['(', ']'], + ['\\left(', '\\right)'], + ['\\left(', '\\right.'], + ['\\left.', '\\right)'], + ['\\left[', '\\right]'], + ['\\left[', '\\right.'], + ['\\left.', '\\right]'], + ['\\left\\{', '\\right\\}'], + ['\\left\\{', '\\right.'], + ['\\left.', '\\right\\}'], + ['\\left<', '\\right>'], + ['\\bigl(', '\\bigr)'], + ['\\bigl[', '\\bigr]'], + ['\\bigl\\{', '\\bigr\\}'], + ['\\Bigl(', '\\Bigr)'], + ['\\Bigl[', '\\Bigr]'], + ['\\Bigl\\{', '\\Bigr\\}'], + ['\\biggl(', '\\biggr)'], + ['\\biggl[', '\\biggr]'], + ['\\biggl\\{', '\\biggr\\}'], + ['\\Biggl(', '\\Biggr)'], + ['\\Biggl[', '\\Biggr]'], + ['\\Biggl\\{', '\\Biggr\\}'], + ['\\langle', '\\rangle'], + ['\\lvert', '\\rvert'], + ['\\lVert', '\\rVert'], + ['\\left|', '\\right|'], + ['\\left\\vert', '\\right\\vert'], + ['\\left\\|', '\\right\\|'], + ['\\left\\Vert', '\\right\\Vert'], + ['\\left\\langle', '\\right\\rangle'], + ['\\left\\lvert', '\\right\\rvert'], + ['\\left\\lVert', '\\right\\rVert'], + ['\\bigl\\langle', '\\bigr\\rangle'], + ['\\bigl|', '\\bigr|'], + ['\\bigl\\vert', '\\bigr\\vert'], + ['\\bigl\\lvert', '\\bigr\\rvert'], + ['\\bigl\\|', '\\bigr\\|'], + ['\\bigl\\lVert', '\\bigr\\rVert'], + ['\\bigl\\Vert', '\\bigr\\Vert'], + ['\\Bigl\\langle', '\\Bigr\\rangle'], + ['\\Bigl|', '\\Bigr|'], + ['\\Bigl\\lvert', '\\Bigr\\rvert'], + ['\\Bigl\\vert', '\\Bigr\\vert'], + ['\\Bigl\\|', '\\Bigr\\|'], + ['\\Bigl\\lVert', '\\Bigr\\rVert'], + ['\\Bigl\\Vert', '\\Bigr\\Vert'], + ['\\biggl\\langle', '\\biggr\\rangle'], + ['\\biggl|', '\\biggr|'], + ['\\biggl\\lvert', '\\biggr\\rvert'], + ['\\biggl\\vert', '\\biggr\\vert'], + ['\\biggl\\|', '\\biggr\\|'], + ['\\biggl\\lVert', '\\biggr\\rVert'], + ['\\biggl\\Vert', '\\biggr\\Vert'], + ['\\Biggl\\langle', '\\Biggr\\rangle'], + ['\\Biggl|', '\\Biggr|'], + ['\\Biggl\\lvert', '\\Biggr\\rvert'], + ['\\Biggl\\vert', '\\Biggr\\vert'], + ['\\Biggl\\|', '\\Biggr\\|'], + ['\\Biggl\\lVert', '\\Biggr\\rVert'], + ['\\Biggl\\Vert', '\\Biggr\\Vert'] +]; + diff --git a/src/vs/editor/test/common/modes/supports/characterPair.test.ts b/src/vs/editor/test/common/modes/supports/characterPair.test.ts index 70c763ec16e..e92b7db2e6e 100644 --- a/src/vs/editor/test/common/modes/supports/characterPair.test.ts +++ b/src/vs/editor/test/common/modes/supports/characterPair.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 90d89185aa4..20170cb8f48 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; diff --git a/src/vs/editor/test/common/modes/supports/indentationRules.ts b/src/vs/editor/test/common/modes/supports/indentationRules.ts index 69dbd2b8fce..0967de48bff 100644 --- a/src/vs/editor/test/common/modes/supports/indentationRules.ts +++ b/src/vs/editor/test/common/modes/supports/indentationRules.ts @@ -25,3 +25,18 @@ export const goIndentationRules = { decreaseIndentPattern: /^\s*(\bcase\b.*:|\bdefault\b:|}[)}]*[),]?|\)[,]?)$/, increaseIndentPattern: /^.*(\bcase\b.*:|\bdefault\b:|(\b(func|if|else|switch|select|for|struct)\b.*)?{[^}"'`]*|\([^)"'`]*)$/, }; + +export const htmlIndentationRules = { + decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/, + increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|keygen|link|menuitem|meta|param|source|track|wbr)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|)|\{[^}"']*$/, +}; + +export const latexIndentationRules = { + decreaseIndentPattern: /^\s*\\end{(?!document)/, + increaseIndentPattern: /\\begin{(?!document)([^}]*)}(?!.*\\end{\1})/, +}; + +export const luaIndentationRules = { + decreaseIndentPattern: /^\s*((\b(elseif|else|end|until)\b)|(\})|(\)))/, + increaseIndentPattern: /^((?!(\-\-)).)*((\b(else|function|then|do|repeat)\b((?!\b(end|until)\b).)*)|(\{\s*))$/, +}; diff --git a/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/src/vs/editor/test/common/modes/supports/onEnter.test.ts index 1daa14e1607..4ed40eb1abb 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { CharacterPair, IndentAction } from 'vs/editor/common/languages/languageConfiguration'; import { OnEnterSupport } from 'vs/editor/common/languages/supports/onEnter'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; diff --git a/src/vs/editor/test/common/modes/supports/onEnterRules.ts b/src/vs/editor/test/common/modes/supports/onEnterRules.ts index a172d113c19..66e683fdcf8 100644 --- a/src/vs/editor/test/common/modes/supports/onEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/onEnterRules.ts @@ -116,6 +116,22 @@ export const cppOnEnterRules = [ } ]; +export const htmlOnEnterRules = [ + { + beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w\-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, + afterText: /^<\/([_:\w][_:\w\-.\d]*)\s*>/i, + action: { + indentAction: IndentAction.IndentOutdent + } + }, + { + beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w\-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, + action: { + indentAction: IndentAction.Indent + } + } +]; + /* export enum IndentAction { None = 0, diff --git a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts index f58f7105d7e..5fd90a71561 100644 --- a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts +++ b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { BracketsUtils } from 'vs/editor/common/languages/supports/richEditBrackets'; diff --git a/src/vs/editor/test/common/modes/supports/tokenization.test.ts b/src/vs/editor/test/common/modes/supports/tokenization.test.ts index 26854898f03..b5386ec1b3b 100644 --- a/src/vs/editor/test/common/modes/supports/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/supports/tokenization.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FontStyle } from 'vs/editor/common/encodedTokenAttributes'; import { ColorMap, ExternalThemeTrieElement, ParsedTokenThemeRule, ThemeTrieElementRule, TokenTheme, parseTokenTheme, strcmp } from 'vs/editor/common/languages/supports/tokenization'; diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index 7593ea14706..af7015a19b6 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ColorId, FontStyle, MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 5dc2dd11f16..781ff450211 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/services/languageService.test.ts b/src/vs/editor/test/common/services/languageService.test.ts index 26a99380e75..e8317e6ae88 100644 --- a/src/vs/editor/test/common/services/languageService.test.ts +++ b/src/vs/editor/test/common/services/languageService.test.ts @@ -3,27 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { throwIfDisposablesAreLeaked } from 'vs/base/test/common/utils'; +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { LanguageService } from 'vs/editor/common/services/languageService'; suite('LanguageService', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('LanguageSelection does not leak a disposable', () => { const languageService = new LanguageService(); - throwIfDisposablesAreLeaked(() => { - const languageSelection = languageService.createById(PLAINTEXT_LANGUAGE_ID); - assert.strictEqual(languageSelection.languageId, PLAINTEXT_LANGUAGE_ID); - }); - throwIfDisposablesAreLeaked(() => { - const languageSelection = languageService.createById(PLAINTEXT_LANGUAGE_ID); - const listener = languageSelection.onDidChange(() => { }); - assert.strictEqual(languageSelection.languageId, PLAINTEXT_LANGUAGE_ID); - listener.dispose(); - }); + const languageSelection1 = languageService.createById(PLAINTEXT_LANGUAGE_ID); + assert.strictEqual(languageSelection1.languageId, PLAINTEXT_LANGUAGE_ID); + const languageSelection2 = languageService.createById(PLAINTEXT_LANGUAGE_ID); + const listener = languageSelection2.onDidChange(() => { }); + assert.strictEqual(languageSelection2.languageId, PLAINTEXT_LANGUAGE_ID); + listener.dispose(); languageService.dispose(); - }); }); diff --git a/src/vs/editor/test/common/services/languagesAssociations.test.ts b/src/vs/editor/test/common/services/languagesAssociations.test.ts index 689457fe678..7d6ce3ba4dc 100644 --- a/src/vs/editor/test/common/services/languagesAssociations.test.ts +++ b/src/vs/editor/test/common/services/languagesAssociations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getMimeTypes, registerPlatformLanguageAssociation, registerConfiguredLanguageAssociation } from 'vs/editor/common/services/languagesAssociations'; diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 74ec1559d43..d4715b8534c 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 53eb701ecfb..cd4b53d86df 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/test/common/services/semanticTokensDto.test.ts b/src/vs/editor/test/common/services/semanticTokensDto.test.ts index b32e7e66c74..7093691179d 100644 --- a/src/vs/editor/test/common/services/semanticTokensDto.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensDto.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { VSBuffer } from 'vs/base/common/buffer'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts index bfec2c00b1a..1128768e919 100644 --- a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts index a40b185fd28..c7fbd1afff2 100644 --- a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts +++ b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IModelService } from 'vs/editor/common/services/model'; @@ -44,13 +44,13 @@ suite('TextResourceConfigurationService - Update', () => { test('updateValue writes without target and overrides when no language is defined', async () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b'); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.USER_LOCAL]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.USER_LOCAL]); }); test('updateValue writes with target and without overrides when no language is defined', async () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b', ConfigurationTarget.USER_LOCAL); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.USER_LOCAL]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.USER_LOCAL]); }); test('updateValue writes into given memory target without overrides', async () => { @@ -63,7 +63,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b', ConfigurationTarget.MEMORY); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.MEMORY]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.MEMORY]); }); test('updateValue writes into given workspace target without overrides', async () => { @@ -76,7 +76,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b', ConfigurationTarget.WORKSPACE); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.WORKSPACE]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.WORKSPACE]); }); test('updateValue writes into given user target without overrides', async () => { @@ -89,7 +89,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b', ConfigurationTarget.USER); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.USER]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.USER]); }); test('updateValue writes into given workspace folder target with overrides', async () => { @@ -98,6 +98,7 @@ suite('TextResourceConfigurationService - Update', () => { default: { value: '1' }, userLocal: { value: '2' }, workspaceFolder: { value: '2', override: '1' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -115,7 +116,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b'); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.WORKSPACE_FOLDER]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.WORKSPACE_FOLDER]); }); test('updateValue writes into derived workspace folder target with overrides', async () => { @@ -125,6 +126,7 @@ suite('TextResourceConfigurationService - Update', () => { userLocal: { value: '2' }, workspace: { value: '2', override: '1' }, workspaceFolder: { value: '2', override: '2' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -142,7 +144,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b'); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.WORKSPACE]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.WORKSPACE]); }); test('updateValue writes into derived workspace target with overrides', async () => { @@ -151,6 +153,7 @@ suite('TextResourceConfigurationService - Update', () => { default: { value: '1' }, userLocal: { value: '2' }, workspace: { value: '2', override: '2' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -165,6 +168,7 @@ suite('TextResourceConfigurationService - Update', () => { userLocal: { value: '2' }, workspace: { value: '2', override: '2' }, workspaceFolder: { value: '2' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -182,7 +186,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b'); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.USER_REMOTE]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.USER_REMOTE]); }); test('updateValue writes into derived user remote target with overrides', async () => { @@ -191,6 +195,7 @@ suite('TextResourceConfigurationService - Update', () => { default: { value: '1' }, userLocal: { value: '2' }, userRemote: { value: '2', override: '3' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -204,7 +209,8 @@ suite('TextResourceConfigurationService - Update', () => { default: { value: '1' }, userLocal: { value: '2' }, userRemote: { value: '2', override: '3' }, - workspace: { value: '3' } + workspace: { value: '3' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -219,7 +225,8 @@ suite('TextResourceConfigurationService - Update', () => { userLocal: { value: '2', override: '1' }, userRemote: { value: '2', override: '3' }, workspace: { value: '3' }, - workspaceFolder: { value: '3' } + workspaceFolder: { value: '3' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -236,7 +243,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b'); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.USER_LOCAL]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.USER_LOCAL]); }); test('updateValue writes into derived user target with overrides', async () => { @@ -244,6 +251,7 @@ suite('TextResourceConfigurationService - Update', () => { configurationValue = { default: { value: '1' }, userLocal: { value: '2', override: '3' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -256,7 +264,8 @@ suite('TextResourceConfigurationService - Update', () => { configurationValue = { default: { value: '1' }, userLocal: { value: '2', override: '3' }, - userRemote: { value: '3' } + userRemote: { value: '3' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -269,7 +278,8 @@ suite('TextResourceConfigurationService - Update', () => { configurationValue = { default: { value: '1' }, userLocal: { value: '2', override: '3' }, - workspaceValue: { value: '3' } + workspaceValue: { value: '3' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -283,7 +293,21 @@ suite('TextResourceConfigurationService - Update', () => { default: { value: '1', override: '3' }, userLocal: { value: '2', override: '3' }, userRemote: { value: '3' }, - workspaceFolderValue: { value: '3' } + workspaceFolderValue: { value: '3' }, + overrideIdentifiers: [language] + }; + const resource = URI.file('someFile'); + + await testObject.updateValue(resource, 'a', '2'); + assert.deepStrictEqual(updateArgs, ['a', '2', { resource, overrideIdentifier: language }, ConfigurationTarget.USER_LOCAL]); + }); + + test('updateValue writes into derived user target when overridden in default and not in user', async () => { + language = 'a'; + configurationValue = { + default: { value: '1', override: '3' }, + userLocal: { value: '2' }, + overrideIdentifiers: [language] }; const resource = URI.file('someFile'); @@ -299,7 +323,7 @@ suite('TextResourceConfigurationService - Update', () => { const resource = URI.file('someFile'); await testObject.updateValue(resource, 'a', 'b'); - assert.deepStrictEqual(updateArgs, ['a', 'b', { resource }, ConfigurationTarget.USER_LOCAL]); + assert.deepStrictEqual(updateArgs, ['a', 'b', { resource, overrideIdentifier: undefined }, ConfigurationTarget.USER_LOCAL]); }); }); diff --git a/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts b/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts index b646a4c18be..9b5351bd618 100644 --- a/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts +++ b/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { UnicodeHighlighterOptions, UnicodeTextModelHighlighter } from 'vs/editor/common/services/unicodeTextModelHighlighter'; diff --git a/src/vs/editor/test/common/view/overviewZoneManager.test.ts b/src/vs/editor/test/common/view/overviewZoneManager.test.ts index 5896b2a928b..b488141d7d7 100644 --- a/src/vs/editor/test/common/view/overviewZoneManager.test.ts +++ b/src/vs/editor/test/common/view/overviewZoneManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ColorZone, OverviewRulerZone, OverviewZoneManager } from 'vs/editor/common/viewModel/overviewZoneManager'; diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index ecfde1e3d91..d1058c42293 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { DecorationSegment, LineDecoration, LineDecorationsNormalizer } from 'vs/editor/common/viewLayout/lineDecorations'; diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index c3f0fa635ab..58f9217ea56 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorWhitespace, LinesLayout } from 'vs/editor/common/viewLayout/linesLayout'; diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 2fdbeaefdf8..22e1c60f7af 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts b/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts index 7c0522a84f0..84659a045c6 100644 --- a/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts +++ b/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { GlyphMarginLanesModel, } from 'vs/editor/common/viewModel/glyphLanesModel'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index 85792a42390..b771b7e5ff8 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel'; diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 37727d4c8a1..a1defd0c4f5 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOptions, WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 1fdb7a2c10b..e1820e6e920 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { toUint32 } from 'vs/base/common/uint'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PrefixSumComputer, PrefixSumIndexOfResult } from 'vs/editor/common/model/prefixSumComputer'; diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts index e99cfa28d94..d2657a7ce41 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import * as fs from 'fs'; // import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; // import { parse } from 'vs/editor/common/modes/tokenization/typescript'; import { toStandardTokenType } from 'vs/editor/common/languages/supports/tokenization'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface IParseFunc { (text: string): number[]; @@ -135,6 +136,9 @@ function executeTest(fileName: string, parseFunc: IParseFunc): void { } suite('Classification', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + test('TypeScript', () => { // executeTest(getPathFromAmdModule(require, 'vs/editor/test/node/classification/typescript-test.ts').replace(/\bout\b/, 'src'), parse); }); diff --git a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index 995472ca78f..72ad7fe2186 100644 --- a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts +++ b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; @@ -17,8 +17,8 @@ suite('myers', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('1', () => { - const s1 = new LinesSliceCharSequence(['hello world'], new OffsetRange(0, 1), true); - const s2 = new LinesSliceCharSequence(['hallo welt'], new OffsetRange(0, 1), true); + const s1 = new LinesSliceCharSequence(['hello world'], new Range(1, 1, 1, Number.MAX_SAFE_INTEGER), true); + const s2 = new LinesSliceCharSequence(['hallo welt'], new Range(1, 1, 1, Number.MAX_SAFE_INTEGER), true); const a = true ? new MyersDiffAlgorithm() : new DynamicProgrammingDiffing(); a.compute(s1, s2); @@ -83,7 +83,7 @@ suite('LinesSliceCharSequence', () => { 'line4: hello world', 'line5: bazz', ], - new OffsetRange(1, 4), true + new Range(2, 1, 5, 1), true ); test('translateOffset', () => { diff --git a/src/vs/editor/test/node/diffing/fixtures.test.ts b/src/vs/editor/test/node/diffing/fixtures.test.ts index e944d133bef..0ed9a8b11fe 100644 --- a/src/vs/editor/test/node/diffing/fixtures.test.ts +++ b/src/vs/editor/test/node/diffing/fixtures.test.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'; import { join, resolve } from 'path'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { FileAccess } from 'vs/base/common/network'; -import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { LegacyLinesDiffComputer } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { Range } from 'vs/editor/common/core/range'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { AbstractText, ArrayText, SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { LinesDiff } from 'vs/editor/common/diff/linesDiffComputer'; suite('diffing fixtures', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -47,7 +49,15 @@ suite('diffing fixtures', () => { const ignoreTrimWhitespace = folder.indexOf('trimws') >= 0; const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: true }); + if (diffingAlgoName === 'advanced' && !ignoreTrimWhitespace) { + assertDiffCorrectness(diff, firstContentLines, secondContentLines); + } + function getDiffs(changes: readonly DetailedLineRangeMapping[]): IDetailedDiff[] { + for (const c of changes) { + RangeMapping.assertSorted(c.innerChanges ?? []); + } + return changes.map(c => ({ originalRange: c.original.toString(), modifiedRange: c.modified.toString(), @@ -123,7 +133,7 @@ suite('diffing fixtures', () => { } test(`test`, () => { - runTest('shifting-twice', 'advanced'); + runTest('invalid-diff-trimws', 'advanced'); }); for (const folder of folders) { @@ -160,3 +170,20 @@ interface IMoveInfo { changes: IDetailedDiff[]; } + +function assertDiffCorrectness(diff: LinesDiff, original: string[], modified: string[]) { + const allInnerChanges = diff.changes.flatMap(c => c.innerChanges!); + const edit = rangeMappingsToTextEdit(allInnerChanges, new ArrayText(modified)); + const result = edit.normalize().apply(new ArrayText(original)); + + assert.deepStrictEqual(result, modified.join('\n')); +} + +function rangeMappingsToTextEdit(rangeMappings: readonly RangeMapping[], modified: AbstractText): TextEdit { + return new TextEdit(rangeMappings.map(m => { + return new SingleTextEdit( + m.originalRange, + modified.getValueOfRange(m.modifiedRange) + ); + })); +} diff --git a/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json index c1c5787e10f..0546ca3cdbe 100644 --- a/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/deletion/advanced.expected.diff.json @@ -13,7 +13,7 @@ "modifiedRange": "[1,2)", "innerChanges": [ { - "originalRange": "[1,1 -> 29,64]", + "originalRange": "[1,1 -> 29,2 EOL]", "modifiedRange": "[1,1 -> 1,1 EOL]" } ] diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json index bdaa293acd4..b9b792ab068 100644 --- a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json @@ -13,7 +13,7 @@ "modifiedRange": "[742,751)", "innerChanges": [ { - "originalRange": "[742,3 -> 742,3]", + "originalRange": "[742,1 -> 742,1]", "modifiedRange": "[742,1 -> 743,8]" }, { diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/1.tst b/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/1.tst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/2.tst b/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/2.tst new file mode 100644 index 00000000000..fe38675389f --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/2.tst @@ -0,0 +1,3 @@ +Tempor duis sunt laborum aliqua id eu irure. +Consequat aliquip excepteur. +Adipisicing incididunt do magn. diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/advanced.expected.diff.json new file mode 100644 index 00000000000..9cefc1c3e20 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/advanced.expected.diff.json @@ -0,0 +1,22 @@ +{ + "original": { + "content": "", + "fileName": "./1.tst" + }, + "modified": { + "content": "Tempor duis sunt laborum aliqua id eu irure.\nConsequat aliquip excepteur.\nAdipisicing incididunt do magn.\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[1,2)", + "modifiedRange": "[1,5)", + "innerChanges": [ + { + "originalRange": "[1,1 -> 1,1 EOL]", + "modifiedRange": "[1,1 -> 4,1 EOL]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/legacy.expected.diff.json new file mode 100644 index 00000000000..34e6d36e9e1 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-ranges/legacy.expected.diff.json @@ -0,0 +1,17 @@ +{ + "original": { + "content": "", + "fileName": "./1.tst" + }, + "modified": { + "content": "Tempor duis sunt laborum aliqua id eu irure.\nConsequat aliquip excepteur.\nAdipisicing incididunt do magn.\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[1,2)", + "modifiedRange": "[1,5)", + "innerChanges": null + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt b/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt new file mode 100644 index 00000000000..db510b75635 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt @@ -0,0 +1,2 @@ +hello world; +y \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt b/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt new file mode 100644 index 00000000000..0dc735e1c5a --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt @@ -0,0 +1,3 @@ +hello world; +// new line +y \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json new file mode 100644 index 00000000000..181c78999fa --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json @@ -0,0 +1,26 @@ +{ + "diffs": [ + { + "innerChanges": [ + { + "modifiedRange": "[1,13 -> 1,13 EOL]", + "originalRange": "[1,13 -> 1,14 EOL]" + }, + { + "modifiedRange": "[2,1 -> 3,1]", + "originalRange": "[2,1 -> 2,1]" + } + ], + "modifiedRange": "[1,3)", + "originalRange": "[1,2)" + } + ], + "modified": { + "content": "hello world;\n// new line\ny", + "fileName": "./2.txt" + }, + "original": { + "content": "hello world; \ny", + "fileName": "./1.txt" + } +} diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json new file mode 100644 index 00000000000..727c2e8eb55 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json @@ -0,0 +1,17 @@ +{ + "original": { + "content": "hello world; \ny", + "fileName": "./1.txt" + }, + "modified": { + "content": "hello world;\n// new line\ny", + "fileName": "./2.txt" + }, + "diffs": [ + { + "originalRange": "[1,2)", + "modifiedRange": "[1,3)", + "innerChanges": null + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/1.tst b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/1.tst new file mode 100644 index 00000000000..7d4c1415308 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/1.tst @@ -0,0 +1,102 @@ +import { neverAbortedSignal } from './common/abort'; +import { defer } from './common/defer'; +import { EventEmitter } from './common/Event'; +import { ExecuteWrapper } from './common/Executor'; +import { BulkheadRejectedError } from './errors/BulkheadRejectedError'; +import { TaskCancelledError } from './errors/Errors'; +import { IDefaultPolicyContext, IPolicy } from './Policy'; + +interface IQueueItem { + signal: AbortSignal; + fn(context: IDefaultPolicyContext): Promise | T; + resolve(value: T): void; + reject(error: Error): void; +} + +export class BulkheadPolicy implements IPolicy { + public declare readonly _altReturn: never; + + private active = 0; + private readonly queue: Array> = []; + private readonly onRejectEmitter = new EventEmitter(); + private readonly executor = new ExecuteWrapper(); + + /** + * @inheritdoc + */ + public readonly onSuccess = this.executor.onSuccess; + + /** + * @inheritdoc + */ + public readonly onFailure = this.executor.onFailure; + + /** + * Emitter that fires when an item is rejected from the bulkhead. + */ + public readonly onReject = this.onRejectEmitter.addListener; + + /** + * Returns the number of available execution slots at this point in time. + */ + public get executionSlots() { + return this.capacity - this.active; + } + + /** + * Returns the number of queue slots at this point in time. + */ + public get queueSlots() { + return this.queueCapacity - this.queue.length; + } + + /** + * Bulkhead limits concurrent requests made. + */ + constructor(private readonly capacity: number, private readonly queueCapacity: number) { } + + /** + * Executes the given function. + * @param fn Function to execute + * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded + */ + public async execute( + fn: (context: IDefaultPolicyContext) => PromiseLike | T, + signal = neverAbortedSignal, + ): Promise { + if (signal.aborted) { + throw new TaskCancelledError(); + } + + if (this.active < this.capacity) { + this.active++; + try { + return await fn({ signal }); + } finally { + this.active--; + this.dequeue(); + } + } + + if (this.queue.length > this.queueCapacity) { + const { resolve, reject, promise } = defer(); + this.queue.push({ signal, fn, resolve, reject }); + return promise; + } + + this.onRejectEmitter.emit(); + throw new BulkheadRejectedError(this.capacity, this.queueCapacity); + } + + private dequeue() { + const item = this.queue.shift(); + if (!item) { + return; + } + + Promise.resolve() + .then(() => this.execute(item.fn, item.signal)) + .then(item.resolve) + .catch(item.reject); + } +} diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/2.tst b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/2.tst new file mode 100644 index 00000000000..9b3687d776a --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/2.tst @@ -0,0 +1,87 @@ +import { neverAbortedSignal } from './common/abort'; +import { defer } from './common/defer'; +import { EventEmitter } from './common/Event'; +import { ExecuteWrapper } from './common/Executor'; +import { BulkheadRejectedError } from './errors/BulkheadRejectedError'; +import { TaskCancelledError } from './errors/Errors'; +import { IDefaultPolicyContext, IPolicy } from './Policy'; + +interface IQueueItem { + signal: AbortSignal; + fn(context: IDefaultPolicyContext): Promise | T; + resolve(value: T): void; + reject(error: Error): void; +} + +export class BulkheadPolicy implements IPolicy { + public declare readonly _altReturn: never; + + private active = 0; + private readonly queue: Array> = []; + private readonly onRejectEmitter = new EventEmitter(); + private readonly executor = new ExecuteWrapper(); + + /** + * @inheritdoc + */ + public readonly onSuccess = this.executor.onSuccess; + + /** + * @inheritdoc + */ + public readonly onFailure = this.executor.onFailure; + + /** + * Emitter that fires when an item is rejected from the bulkhead. + */ + public readonly onReject = this.onRejectEmitter.addListener; + + /** + * Returns the number of available execution slots at this point in time. + */ + public get executionSlots() { + return this.capacity - this.active; + } + + /** + * Returns the number of queue slots at this point in time. + */ + public get queueSlots() { + return this.queueCapacity - this.queue.length; + } + + /** + * Bulkhead limits concurrent requests made. + */ + constructor(private readonly capacity: number, private readonly queueCapacity: number) { } + + /** + * Executes the given function. + * @param fn Function to execute + * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded + */ + public async execute( + fn: (context: IDefaultPolicyContext) => PromiseLike | T, + signal = neverAbortedSignal, + ): Promise { + if (signal.aborted) { + throw new TaskCancelledError(); + } + + if (this.active < this.capacity) { + this.active++; + try { + return await fn({ signal }); + } finally { + this.active--; + this.dequeue(); + } + } + + if (this.queue.length >= this.queueCapacity) { + this.onRejectEmitter.emit(); + throw new BulkheadRejectedError(this.capacity, this.queueCapacity); + } + const { resolve, reject, promise } = defer(); + this.queue.push({ signal, fn, resolve, reject }); + return promise; diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/advanced.expected.diff.json new file mode 100644 index 00000000000..06f0ca747cf --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/advanced.expected.diff.json @@ -0,0 +1,42 @@ +{ + "original": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length > this.queueCapacity) {\n\t\t\tconst { resolve, reject, promise } = defer();\n\t\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\t\treturn promise;\n\t\t}\n\n\t\tthis.onRejectEmitter.emit();\n\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t}\n\n\tprivate dequeue() {\n\t\tconst item = this.queue.shift();\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\n\t\tPromise.resolve()\n\t\t\t.then(() => this.execute(item.fn, item.signal))\n\t\t\t.then(item.resolve)\n\t\t\t.catch(item.reject);\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length >= this.queueCapacity) {\n\t\t\tthis.onRejectEmitter.emit();\n\t\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t\t}\n\t\tconst { resolve, reject, promise } = defer();\n\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\treturn promise;\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[81,103)", + "modifiedRange": "[81,88)", + "innerChanges": [ + { + "originalRange": "[81,26 -> 81,26]", + "modifiedRange": "[81,26 -> 81,27]" + }, + { + "originalRange": "[82,1 -> 82,1]", + "modifiedRange": "[82,1 -> 85,1]" + }, + { + "originalRange": "[82,1 -> 82,2]", + "modifiedRange": "[85,1 -> 85,1]" + }, + { + "originalRange": "[83,1 -> 83,2]", + "modifiedRange": "[86,1 -> 86,1]" + }, + { + "originalRange": "[84,1 -> 84,2]", + "modifiedRange": "[87,1 -> 87,1]" + }, + { + "originalRange": "[85,1 -> 103,1 EOL]", + "modifiedRange": "[88,1 -> 88,1 EOL]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/legacy.expected.diff.json new file mode 100644 index 00000000000..88d6c0c6cf8 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/legacy.expected.diff.json @@ -0,0 +1,74 @@ +{ + "original": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length > this.queueCapacity) {\n\t\t\tconst { resolve, reject, promise } = defer();\n\t\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\t\treturn promise;\n\t\t}\n\n\t\tthis.onRejectEmitter.emit();\n\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t}\n\n\tprivate dequeue() {\n\t\tconst item = this.queue.shift();\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\n\t\tPromise.resolve()\n\t\t\t.then(() => this.execute(item.fn, item.signal))\n\t\t\t.then(item.resolve)\n\t\t\t.catch(item.reject);\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length >= this.queueCapacity) {\n\t\t\tthis.onRejectEmitter.emit();\n\t\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t\t}\n\t\tconst { resolve, reject, promise } = defer();\n\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\treturn promise;\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[81,103)", + "modifiedRange": "[81,88)", + "innerChanges": [ + { + "originalRange": "[81,26 -> 81,26]", + "modifiedRange": "[81,26 -> 81,27]" + }, + { + "originalRange": "[81,48 -> 86,1 EOL]", + "modifiedRange": "[81,49 -> 81,49 EOL]" + }, + { + "originalRange": "[87,1 -> 87,1]", + "modifiedRange": "[82,1 -> 82,2]" + }, + { + "originalRange": "[88,1 -> 88,1]", + "modifiedRange": "[83,1 -> 83,2]" + }, + { + "originalRange": "[89,1 -> 89,1]", + "modifiedRange": "[84,1 -> 84,2]" + }, + { + "originalRange": "[90,1 -> 92,1]", + "modifiedRange": "[85,1 -> 85,1]" + }, + { + "originalRange": "[92,9 -> 97,4]", + "modifiedRange": "[85,9 -> 85,29]" + }, + { + "originalRange": "[97,10 -> 97,20 EOL]", + "modifiedRange": "[85,35 -> 85,51 EOL]" + }, + { + "originalRange": "[98,3 -> 98,16]", + "modifiedRange": "[86,3 -> 86,3]" + }, + { + "originalRange": "[98,21 -> 98,43]", + "modifiedRange": "[86,8 -> 86,21]" + }, + { + "originalRange": "[98,49 -> 99,15]", + "modifiedRange": "[86,27 -> 86,33]" + }, + { + "originalRange": "[99,22 -> 100,16]", + "modifiedRange": "[86,40 -> 86,42]" + }, + { + "originalRange": "[100,22 -> 100,22]", + "modifiedRange": "[86,48 -> 86,50]" + }, + { + "originalRange": "[101,2 -> 102,2 EOL]", + "modifiedRange": "[87,2 -> 87,18 EOL]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index ae56ff86935..be2c930e9eb 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2919,6 +2919,9 @@ declare namespace monaco.editor { * An event describing a change in the text of a model. */ export interface IModelContentChangedEvent { + /** + * The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence. + */ readonly changes: IModelContentChange[]; /** * The (new) end-of-line character. @@ -3735,6 +3738,11 @@ declare namespace monaco.editor { * Defaults to false. */ peekWidgetDefaultFocus?: 'tree' | 'editor'; + /** + * Sets a placeholder for the editor. + * If set, the placeholder is shown if the editor is empty. + */ + placeholder?: string | undefined; /** * Controls whether the definition link opens element in the peek widget. * Defaults to false. @@ -3816,6 +3824,11 @@ declare namespace monaco.editor { * and the diff editor has a width less than `renderSideBySideInlineBreakpoint`, the inline view is used. */ useInlineViewWhenSpaceIsLimited?: boolean; + /** + * If set, the diff editor is optimized for small views. + * Defaults to `false`. + */ + compactMode?: boolean; /** * Timeout in milliseconds after which diff computation is cancelled. * Defaults to 5000. @@ -3878,6 +3891,10 @@ declare namespace monaco.editor { */ showMoves?: boolean; showEmptyDecorations?: boolean; + /** + * Only applies when `renderSideBySide` is set to false. + */ + useTrueInlineView?: boolean; }; /** * Is the diff editor inside another editor @@ -4043,11 +4060,13 @@ declare namespace monaco.editor { multipleDeclarations?: GoToLocationValues; multipleImplementations?: GoToLocationValues; multipleReferences?: GoToLocationValues; + multipleTests?: GoToLocationValues; alternativeDefinitionCommand?: string; alternativeTypeDefinitionCommand?: string; alternativeDeclarationCommand?: string; alternativeImplementationCommand?: string; alternativeReferenceCommand?: string; + alternativeTestsCommand?: string; } /** @@ -4318,6 +4337,10 @@ declare namespace monaco.editor { * Font size of section headers. Defaults to 9. */ sectionHeaderFontSize?: number; + /** + * Spacing between the section header characters (in CSS px). Defaults to 1. + */ + sectionHeaderLetterSpacing?: number; } /** @@ -4562,7 +4585,6 @@ declare namespace monaco.editor { * Does not clear active inline suggestions when the editor loses focus. */ keepOnBlur?: boolean; - backgroundColoring?: boolean; } export interface IBracketPairColorizationOptions { @@ -4925,68 +4947,69 @@ declare namespace monaco.editor { pasteAs = 85, parameterHints = 86, peekWidgetDefaultFocus = 87, - definitionLinkOpensInPeek = 88, - quickSuggestions = 89, - quickSuggestionsDelay = 90, - readOnly = 91, - readOnlyMessage = 92, - renameOnType = 93, - renderControlCharacters = 94, - renderFinalNewline = 95, - renderLineHighlight = 96, - renderLineHighlightOnlyWhenFocus = 97, - renderValidationDecorations = 98, - renderWhitespace = 99, - revealHorizontalRightPadding = 100, - roundedSelection = 101, - rulers = 102, - scrollbar = 103, - scrollBeyondLastColumn = 104, - scrollBeyondLastLine = 105, - scrollPredominantAxis = 106, - selectionClipboard = 107, - selectionHighlight = 108, - selectOnLineNumbers = 109, - showFoldingControls = 110, - showUnused = 111, - snippetSuggestions = 112, - smartSelect = 113, - smoothScrolling = 114, - stickyScroll = 115, - stickyTabStops = 116, - stopRenderingLineAfter = 117, - suggest = 118, - suggestFontSize = 119, - suggestLineHeight = 120, - suggestOnTriggerCharacters = 121, - suggestSelection = 122, - tabCompletion = 123, - tabIndex = 124, - unicodeHighlighting = 125, - unusualLineTerminators = 126, - useShadowDOM = 127, - useTabStops = 128, - wordBreak = 129, - wordSegmenterLocales = 130, - wordSeparators = 131, - wordWrap = 132, - wordWrapBreakAfterCharacters = 133, - wordWrapBreakBeforeCharacters = 134, - wordWrapColumn = 135, - wordWrapOverride1 = 136, - wordWrapOverride2 = 137, - wrappingIndent = 138, - wrappingStrategy = 139, - showDeprecated = 140, - inlayHints = 141, - editorClassName = 142, - pixelRatio = 143, - tabFocusMode = 144, - layoutInfo = 145, - wrappingInfo = 146, - defaultColorDecorators = 147, - colorDecoratorsActivatedOn = 148, - inlineCompletionsAccessibilityVerbose = 149 + placeholder = 88, + definitionLinkOpensInPeek = 89, + quickSuggestions = 90, + quickSuggestionsDelay = 91, + readOnly = 92, + readOnlyMessage = 93, + renameOnType = 94, + renderControlCharacters = 95, + renderFinalNewline = 96, + renderLineHighlight = 97, + renderLineHighlightOnlyWhenFocus = 98, + renderValidationDecorations = 99, + renderWhitespace = 100, + revealHorizontalRightPadding = 101, + roundedSelection = 102, + rulers = 103, + scrollbar = 104, + scrollBeyondLastColumn = 105, + scrollBeyondLastLine = 106, + scrollPredominantAxis = 107, + selectionClipboard = 108, + selectionHighlight = 109, + selectOnLineNumbers = 110, + showFoldingControls = 111, + showUnused = 112, + snippetSuggestions = 113, + smartSelect = 114, + smoothScrolling = 115, + stickyScroll = 116, + stickyTabStops = 117, + stopRenderingLineAfter = 118, + suggest = 119, + suggestFontSize = 120, + suggestLineHeight = 121, + suggestOnTriggerCharacters = 122, + suggestSelection = 123, + tabCompletion = 124, + tabIndex = 125, + unicodeHighlighting = 126, + unusualLineTerminators = 127, + useShadowDOM = 128, + useTabStops = 129, + wordBreak = 130, + wordSegmenterLocales = 131, + wordSeparators = 132, + wordWrap = 133, + wordWrapBreakAfterCharacters = 134, + wordWrapBreakBeforeCharacters = 135, + wordWrapColumn = 136, + wordWrapOverride1 = 137, + wordWrapOverride2 = 138, + wrappingIndent = 139, + wrappingStrategy = 140, + showDeprecated = 141, + inlayHints = 142, + editorClassName = 143, + pixelRatio = 144, + tabFocusMode = 145, + layoutInfo = 146, + wrappingInfo = 147, + defaultColorDecorators = 148, + colorDecoratorsActivatedOn = 149, + inlineCompletionsAccessibilityVerbose = 150 } export const EditorOptions: { @@ -4999,8 +5022,8 @@ declare namespace monaco.editor { screenReaderAnnounceInlineSuggestion: IEditorOption; autoClosingBrackets: IEditorOption; autoClosingComments: IEditorOption; - autoClosingDelete: IEditorOption; - autoClosingOvertype: IEditorOption; + autoClosingDelete: IEditorOption; + autoClosingOvertype: IEditorOption; autoClosingQuotes: IEditorOption; autoIndent: IEditorOption; automaticLayout: IEditorOption; @@ -5012,7 +5035,7 @@ declare namespace monaco.editor { codeLensFontFamily: IEditorOption; codeLensFontSize: IEditorOption; colorDecorators: IEditorOption; - colorDecoratorActivatedOn: IEditorOption; + colorDecoratorActivatedOn: IEditorOption; colorDecoratorsLimit: IEditorOption; columnSelection: IEditorOption; comments: IEditorOption>>; @@ -5079,6 +5102,7 @@ declare namespace monaco.editor { pasteAs: IEditorOption>>; parameterHints: IEditorOption>>; peekWidgetDefaultFocus: IEditorOption; + placeholder: IEditorOption; definitionLinkOpensInPeek: IEditorOption; quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; @@ -5120,13 +5144,13 @@ declare namespace monaco.editor { tabCompletion: IEditorOption; tabIndex: IEditorOption; unicodeHighlight: IEditorOption; - unusualLineTerminators: IEditorOption; + unusualLineTerminators: IEditorOption; useShadowDOM: IEditorOption; useTabStops: IEditorOption; wordBreak: IEditorOption; wordSegmenterLocales: IEditorOption; wordSeparators: IEditorOption; - wordWrap: IEditorOption; + wordWrap: IEditorOption; wordWrapBreakAfterCharacters: IEditorOption; wordWrapBreakBeforeCharacters: IEditorOption; wordWrapColumn: IEditorOption; @@ -5387,12 +5411,21 @@ declare namespace monaco.editor { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: IEvent; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ @@ -5818,6 +5851,18 @@ declare namespace monaco.editor { * @event */ readonly onDidChangeHiddenAreas: IEvent; + /** + * Some editor operations fire multiple events at once. + * To allow users to react to multiple events fired by a single operation, + * the editor fires a begin update before the operation and an end update after the operation. + * Whenever the editor fires `onBeginUpdate`, it will also fire `onEndUpdate` once the operation finishes. + * Note that not all operations are bracketed by `onBeginUpdate` and `onEndUpdate`. + */ + readonly onBeginUpdate: IEvent; + /** + * Fires after the editor completes the operation it fired `onBeginUpdate` for. + */ + readonly onEndUpdate: IEvent; /** * Saves current view state of the editor in a serializable object. */ @@ -6832,19 +6877,56 @@ declare namespace monaco.languages { * current position itself. */ range?: IRange; + /** + * Can increase the verbosity of the hover + */ + canIncreaseVerbosity?: boolean; + /** + * Can decrease the verbosity of the hover + */ + canDecreaseVerbosity?: boolean; } /** * The hover provider interface defines the contract between extensions and * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ - export interface HoverProvider { + export interface HoverProvider { /** - * Provide a hover for the given position and document. Multiple hovers at the same + * Provide a hover for the given position, context and document. Multiple hovers at the same * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. */ - provideHover(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideHover(model: editor.ITextModel, position: Position, token: CancellationToken, context?: HoverContext): ProviderResult; + } + + export interface HoverContext { + /** + * Hover verbosity request + */ + verbosityRequest?: HoverVerbosityRequest; + } + + export interface HoverVerbosityRequest { + /** + * The delta by which to increase/decrease the hover verbosity level + */ + verbosityDelta: number; + /** + * The previous hover for the same position + */ + previousHover: THover; + } + + export enum HoverVerbosityAction { + /** + * Increase the verbosity of the hover + */ + Increase = 0, + /** + * Decrease the verbosity of the hover + */ + Decrease = 1 } export enum CompletionItemKind { @@ -7358,7 +7440,7 @@ declare namespace monaco.languages { * A provider that can provide document highlights across multiple documents. */ export interface MultiDocumentHighlightProvider { - selector: LanguageFilter; + readonly selector: LanguageSelector; /** * Provide a Map of Uri --> document highlights, like all occurrences of a variable or * all exit-points of a function. @@ -7855,13 +7937,19 @@ declare namespace monaco.languages { AIGenerated = 1 } + export enum NewSymbolNameTriggerKind { + Invoke = 0, + Automatic = 1 + } + export interface NewSymbolName { readonly newSymbolName: string; readonly tags?: readonly NewSymbolNameTag[]; } export interface NewSymbolNamesProvider { - provideNewSymbolNames(model: editor.ITextModel, range: IRange, token: CancellationToken): ProviderResult; + supportsAutomaticNewSymbolNamesTriggerKind?: Promise; + provideNewSymbolNames(model: editor.ITextModel, range: IRange, triggerKind: NewSymbolNameTriggerKind, token: CancellationToken): ProviderResult; } export interface Command { @@ -7871,6 +7959,11 @@ declare namespace monaco.languages { arguments?: any[]; } + export interface CommentThreadRevealOptions { + preserveFocus: boolean; + focusReply: boolean; + } + export interface CommentAuthorInformation { name: string; iconPath?: UriComponents; @@ -7985,7 +8078,7 @@ declare namespace monaco.languages { * * @param document The document to provide mapped edits for. * @param codeBlocks Code blocks that come from an LLM's reply. - * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. * @param context The context for providing mapped edits. * @param token A cancellation token. * @returns A provider result of text edits. diff --git a/src/vs/nls.build.ts b/src/vs/nls.build.ts deleted file mode 100644 index 05c063fa291..00000000000 --- a/src/vs/nls.build.ts +++ /dev/null @@ -1,88 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const buildMap: { [name: string]: string[] } = {}; -const buildMapKeys: { [name: string]: string[] } = {}; -const entryPoints: { [entryPoint: string]: string[] } = {}; - -export interface ILocalizeInfo { - key: string; - comment: string[]; -} - -export function localize(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string { - throw new Error(`Not supported at build time!`); -} - -export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): never { - throw new Error(`Not supported at build time!`); -} - -export function getConfiguredDefaultLocale(): string | undefined { - throw new Error(`Not supported at build time!`); -} - -/** - * Invoked by the loader at build-time - */ -export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoader.IPluginLoadCallback, config: AMDLoader.IConfigurationOptions): void { - if (!name || name.length === 0) { - load({ localize, localize2, getConfiguredDefaultLocale }); - } else { - req([name + '.nls', name + '.nls.keys'], function (messages: string[], keys: string[]) { - buildMap[name] = messages; - buildMapKeys[name] = keys; - load(messages); - }); - } -} - -/** - * Invoked by the loader at build-time - */ -export function write(pluginName: string, moduleName: string, write: AMDLoader.IPluginWriteCallback): void { - const entryPoint = write.getEntryPoint(); - - entryPoints[entryPoint] = entryPoints[entryPoint] || []; - entryPoints[entryPoint].push(moduleName); - - if (moduleName !== entryPoint) { - write.asModule(pluginName + '!' + moduleName, 'define([\'vs/nls\', \'vs/nls!' + entryPoint + '\'], function(nls, data) { return nls.create("' + moduleName + '", data); });'); - } -} - -/** - * Invoked by the loader at build-time - */ -export function writeFile(pluginName: string, moduleName: string, req: AMDLoader.IRelativeRequire, write: AMDLoader.IPluginWriteFileCallback, config: AMDLoader.IConfigurationOptions): void { - if (entryPoints.hasOwnProperty(moduleName)) { - const fileName = req.toUrl(moduleName + '.nls.js'); - const contents = [ - '/*---------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' *--------------------------------------------------------*/' - ], - entries = entryPoints[moduleName]; - - const data: { [moduleName: string]: string[] } = {}; - for (let i = 0; i < entries.length; i++) { - data[entries[i]] = buildMap[entries[i]]; - } - - contents.push('define("' + moduleName + '.nls", ' + JSON.stringify(data, null, '\t') + ');'); - write(fileName, contents.join('\r\n')); - } -} - -/** - * Invoked by the loader at build-time - */ -export function finishBuild(write: AMDLoader.IPluginWriteFileCallback): void { - write('nls.metadata.json', JSON.stringify({ - keys: buildMapKeys, - messages: buildMap, - bundles: entryPoints - }, null, '\t')); -} diff --git a/src/vs/nls.mock.ts b/src/vs/nls.mock.ts deleted file mode 100644 index 5323c6c6340..00000000000 --- a/src/vs/nls.mock.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface ILocalizeInfo { - key: string; - comment: string[]; -} - -export interface ILocalizedString { - original: string; - value: string; -} - -function _format(message: string, args: any[]): string { - let result: string; - if (args.length === 0) { - result = message; - } else { - result = message.replace(/\{(\d+)\}/g, function (match, rest) { - const index = rest[0]; - return typeof args[index] !== 'undefined' ? args[index] : match; - }); - } - return result; -} - -export function localize(data: ILocalizeInfo | string, message: string, ...args: any[]): string { - return _format(message, args); -} - -export function localize2(data: ILocalizeInfo | string, message: string, ...args: any[]): ILocalizedString { - const res = _format(message, args); - return { - original: res, - value: res - }; -} - -export function getConfiguredDefaultLocale(_: string) { - return undefined; -} diff --git a/src/vs/nls.ts b/src/vs/nls.ts index 233840e65ab..8303d8a32e2 100644 --- a/src/vs/nls.ts +++ b/src/vs/nls.ts @@ -3,27 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -let isPseudo = (typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0); -const DEFAULT_TAG = 'i-default'; - -interface INLSPluginConfig { - availableLanguages?: INLSPluginConfigAvailableLanguages; - loadBundle?: BundleLoader; - translationServiceUrl?: string; -} - -export interface INLSPluginConfigAvailableLanguages { - '*'?: string; - [module: string]: string | undefined; -} - -interface BundleLoader { - (bundle: string, locale: string | null, cb: (err: Error, messages: string[] | IBundledStrings) => void): void; -} - -interface IBundledStrings { - [moduleId: string]: string[]; -} +const isPseudo = globalThis._VSCODE_NLS_LANGUAGE === 'pseudo' || (typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0); export interface ILocalizeInfo { key: string; @@ -35,30 +15,6 @@ export interface ILocalizedString { value: string; } -interface ILocalizeFunc { - (info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string; - (key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string; -} - -interface IBoundLocalizeFunc { - (idx: number, defaultValue: null): string; -} - -interface ILocalize2Func { - (info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString; - (key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString; -} - -interface IBoundLocalize2Func { - (idx: number, defaultValue: string): ILocalizedString; -} - -interface IConsumerAPI { - localize: ILocalizeFunc | IBoundLocalizeFunc; - localize2: ILocalize2Func | IBoundLocalize2Func; - getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined; -} - function _format(message: string, args: (string | number | boolean | undefined | null)[]): string { let result: string; @@ -86,49 +42,6 @@ function _format(message: string, args: (string | number | boolean | undefined | return result; } -function findLanguageForModule(config: INLSPluginConfigAvailableLanguages, name: string) { - let result = config[name]; - if (result) { - return result; - } - result = config['*']; - if (result) { - return result; - } - return null; -} - -function endWithSlash(path: string): string { - if (path.charAt(path.length - 1) === '/') { - return path; - } - return path + '/'; -} - -async function getMessagesFromTranslationsService(translationServiceUrl: string, language: string, name: string): Promise { - const url = endWithSlash(translationServiceUrl) + endWithSlash(language) + 'vscode/' + endWithSlash(name); - const res = await fetch(url); - if (res.ok) { - const messages = await res.json() as string[] | IBundledStrings; - return messages; - } - throw new Error(`${res.status} - ${res.statusText}`); -} - -function createScopedLocalize(scope: string[]): IBoundLocalizeFunc { - return function (idx: number, defaultValue: null) { - const restArgs = Array.prototype.slice.call(arguments, 2); - return _format(scope[idx], restArgs); - }; -} - -function createScopedLocalize2(scope: string[]): IBoundLocalize2Func { - return (idx: number, defaultValue: string, ...args) => ({ - value: _format(scope[idx], args), - original: _format(defaultValue, args) - }); -} - /** * Marks a string to be localized. Returns the localized string. * @@ -160,10 +73,29 @@ export function localize(key: string, message: string, ...args: (string | number /** * @skipMangle */ -export function localize(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string { +export function localize(data: ILocalizeInfo | string /* | number when built */, message: string /* | null when built */, ...args: (string | number | boolean | undefined | null)[]): string { + if (typeof data === 'number') { + return _format(lookupMessage(data, message), args); + } return _format(message, args); } +/** + * Only used when built: Looks up the message in the global NLS table. + * This table is being made available as a global through bootstrapping + * depending on the target context. + */ +function lookupMessage(index: number, fallback: string | null): string { + const message = globalThis._VSCODE_NLS_MESSAGES?.[index]; + if (typeof message !== 'string') { + if (typeof fallback === 'string') { + return fallback; + } + throw new Error(`!!! NLS MISSING: ${index} !!!`); + } + return message; +} + /** * Marks a string to be localized. Returns an {@linkcode ILocalizedString} * which contains the localized string and the original string. @@ -197,123 +129,107 @@ export function localize2(key: string, message: string, ...args: (string | numbe /** * @skipMangle */ -export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString { - const original = _format(message, args); - return { - value: original, - original - }; -} - -/** - * - * @param stringFromLocalizeCall You must pass in a string that was returned from a `nls.localize()` call - * in order to ensure the loader plugin has been initialized before this function is called. - */ -export function getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined; -/** - * @skipMangle - */ -export function getConfiguredDefaultLocale(_: string): string | undefined { - // This returns undefined because this implementation isn't used and is overwritten by the loader - // when loaded. - return undefined; -} - -/** - * @skipMangle - */ -export function setPseudoTranslation(value: boolean) { - isPseudo = value; -} - -/** - * Invoked in a built product at run-time - * @skipMangle - */ -export function create(key: string, data: IBundledStrings & IConsumerAPI): IConsumerAPI { - return { - localize: createScopedLocalize(data[key]), - localize2: createScopedLocalize2(data[key]), - getConfiguredDefaultLocale: data.getConfiguredDefaultLocale ?? ((_: string) => undefined) - }; -} - -/** - * Invoked by the loader at run-time - * @skipMangle - */ -export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoader.IPluginLoadCallback, config: AMDLoader.IConfigurationOptions): void { - const pluginConfig: INLSPluginConfig = config['vs/nls'] ?? {}; - if (!name || name.length === 0) { - // TODO: We need to give back the mangled names here - return load({ - localize: localize, - localize2: localize2, - getConfiguredDefaultLocale: () => pluginConfig.availableLanguages?.['*'] - } as IConsumerAPI); - } - const language = pluginConfig.availableLanguages ? findLanguageForModule(pluginConfig.availableLanguages, name) : null; - const useDefaultLanguage = language === null || language === DEFAULT_TAG; - let suffix = '.nls'; - if (!useDefaultLanguage) { - suffix = suffix + '.' + language; - } - const messagesLoaded = (messages: string[] | IBundledStrings) => { - if (Array.isArray(messages)) { - (messages as any as IConsumerAPI).localize = createScopedLocalize(messages); - (messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages); - } else { - (messages as any as IConsumerAPI).localize = createScopedLocalize(messages[name]); - (messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages[name]); - } - (messages as any as IConsumerAPI).getConfiguredDefaultLocale = () => pluginConfig.availableLanguages?.['*']; - load(messages); - }; - if (typeof pluginConfig.loadBundle === 'function') { - (pluginConfig.loadBundle as BundleLoader)(name, language, (err: Error, messages) => { - // We have an error. Load the English default strings to not fail - if (err) { - req([name + '.nls'], messagesLoaded); - } else { - messagesLoaded(messages); - } - }); - } else if (pluginConfig.translationServiceUrl && !useDefaultLanguage) { - (async () => { - try { - const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl!, language, name); - return messagesLoaded(messages); - } catch (err) { - // Language is already as generic as it gets, so require default messages - if (!language.includes('-')) { - console.error(err); - return req([name + '.nls'], messagesLoaded); - } - try { - // Since there is a dash, the language configured is a specific sub-language of the same generic language. - // Since we were unable to load the specific language, try to load the generic language. Ex. we failed to find a - // Swiss German (de-CH), so try to load the generic German (de) messages instead. - const genericLanguage = language.split('-')[0]; - const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl!, genericLanguage, name); - // We got some messages, so we configure the configuration to use the generic language for this session. - pluginConfig.availableLanguages ??= {}; - pluginConfig.availableLanguages['*'] = genericLanguage; - return messagesLoaded(messages); - } catch (err) { - console.error(err); - return req([name + '.nls'], messagesLoaded); - } - } - })(); +export function localize2(data: ILocalizeInfo | string /* | number when built */, originalMessage: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString { + let message: string; + if (typeof data === 'number') { + message = lookupMessage(data, originalMessage); } else { - req([name + suffix], messagesLoaded, (err: Error) => { - if (suffix === '.nls') { - console.error('Failed trying to load default language strings', err); - return; - } - console.error(`Failed to load message bundle for language ${language}. Falling back to the default language:`, err); - req([name + '.nls'], messagesLoaded); - }); + message = originalMessage; } + + const value = _format(message, args); + + return { + value, + original: originalMessage === message ? value : _format(originalMessage, args) + }; } + +export interface INLSLanguagePackConfiguration { + + /** + * The path to the translations config file that contains pointers to + * all message bundles for `main` and extensions. + */ + readonly translationsConfigFile: string; + + /** + * The path to the file containing the translations for this language + * pack as flat string array. + */ + readonly messagesFile: string; + + /** + * The path to the file that can be used to signal a corrupt language + * pack, for example when reading the `messagesFile` fails. This will + * instruct the application to re-create the cache on next startup. + */ + readonly corruptMarkerFile: string; +} + +export interface INLSConfiguration { + + /** + * Locale as defined in `argv.json` or `app.getLocale()`. + */ + readonly userLocale: string; + + /** + * Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`). + */ + readonly osLocale: string; + + /** + * The actual language of the UI that ends up being used considering `userLocale` + * and `osLocale`. + */ + readonly resolvedLanguage: string; + + /** + * Defined if a language pack is used that is not the + * default english language pack. This requires a language + * pack to be installed as extension. + */ + readonly languagePack?: INLSLanguagePackConfiguration; + + /** + * The path to the file containing the default english messages + * as flat string array. The file is only present in built + * versions of the application. + */ + readonly defaultMessagesFile: string; + + /** + * Below properties are deprecated and only there to continue support + * for `vscode-nls` module that depends on them. + * Refs https://github.com/microsoft/vscode-nls/blob/main/src/node/main.ts#L36-L46 + */ + /** @deprecated */ + readonly locale: string; + /** @deprecated */ + readonly availableLanguages: Record; + /** @deprecated */ + readonly _languagePackSupport?: boolean; + /** @deprecated */ + readonly _languagePackId?: string; + /** @deprecated */ + readonly _translationsConfigFile?: string; + /** @deprecated */ + readonly _cacheRoot?: string; + /** @deprecated */ + readonly _resolvedLanguagePackCoreLocation?: string; + /** @deprecated */ + readonly _corruptedFile?: string; +} + +export interface ILanguagePack { + readonly hash: string; + readonly label: string | undefined; + readonly extensions: { + readonly extensionIdentifier: { readonly id: string; readonly uuid?: string }; + readonly version: string; + }[]; + readonly translations: Record; +} + +export type ILanguagePacks = Record; diff --git a/src/vs/platform/accessibility/browser/accessibilityService.ts b/src/vs/platform/accessibility/browser/accessibilityService.ts index bd84abbc6dc..408fbc07b2a 100644 --- a/src/vs/platform/accessibility/browser/accessibilityService.ts +++ b/src/vs/platform/accessibility/browser/accessibilityService.ts @@ -24,6 +24,9 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe protected _systemMotionReduced: boolean; protected readonly _onDidChangeReducedMotion = new Emitter(); + private _linkUnderlinesEnabled: boolean; + protected readonly _onDidChangeLinkUnderline = new Emitter(); + constructor( @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILayoutService private readonly _layoutService: ILayoutService, @@ -50,7 +53,10 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._systemMotionReduced = reduceMotionMatcher.matches; this._configMotionReduced = this._configurationService.getValue<'auto' | 'on' | 'off'>('workbench.reduceMotion'); + this._linkUnderlinesEnabled = this._configurationService.getValue('accessibility.underlineLinks'); + this.initReducedMotionListeners(reduceMotionMatcher); + this.initLinkUnderlineListeners(); } private initReducedMotionListeners(reduceMotionMatcher: MediaQueryList) { @@ -72,6 +78,29 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._register(this.onDidChangeReducedMotion(() => updateRootClasses())); } + private initLinkUnderlineListeners() { + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('accessibility.underlineLinks')) { + const linkUnderlinesEnabled = this._configurationService.getValue('accessibility.underlineLinks'); + this._linkUnderlinesEnabled = linkUnderlinesEnabled; + this._onDidChangeLinkUnderline.fire(); + } + })); + + const updateLinkUnderlineClasses = () => { + const underlineLinks = this._linkUnderlinesEnabled; + this._layoutService.mainContainer.classList.toggle('underline-links', underlineLinks); + }; + + updateLinkUnderlineClasses(); + + this._register(this.onDidChangeLinkUnderlines(() => updateLinkUnderlineClasses())); + } + + public onDidChangeLinkUnderlines(listener: () => void) { + return this._onDidChangeLinkUnderline.event(listener); + } + get onDidChangeScreenReaderOptimized(): Event { return this._onDidChangeScreenReaderOptimized.event; } diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts new file mode 100644 index 00000000000..071afc8fa1f --- /dev/null +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/platform/instantiation/common/instantiation'; +import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { Event } from 'vs/base/common/event'; +import { IAction } from 'vs/base/common/actions'; +import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; + +export const IAccessibleViewService = createDecorator('accessibleViewService'); + +export const enum AccessibleViewProviderId { + Terminal = 'terminal', + TerminalChat = 'terminal-chat', + TerminalHelp = 'terminal-help', + DiffEditor = 'diffEditor', + Chat = 'panelChat', + InlineChat = 'inlineChat', + InlineCompletions = 'inlineCompletions', + KeybindingsEditor = 'keybindingsEditor', + Notebook = 'notebook', + Editor = 'editor', + Hover = 'hover', + Notification = 'notification', + EmptyEditorHint = 'emptyEditorHint', + Comments = 'comments', + Repl = 'repl', + ReplHelp = 'replHelp', + RunAndDebug = 'runAndDebug', +} + +export const enum AccessibleViewType { + Help = 'help', + View = 'view' +} + +export const enum NavigationType { + Previous = 'previous', + Next = 'next' +} + +export interface IAccessibleViewOptions { + readMoreUrl?: string; + /** + * Defaults to markdown + */ + language?: string; + type: AccessibleViewType; + /** + * By default, places the cursor on the top line of the accessible view. + * If set to 'initial-bottom', places the cursor on the bottom line of the accessible view and preserves it henceforth. + * If set to 'bottom', places the cursor on the bottom line of the accessible view. + */ + position?: 'bottom' | 'initial-bottom'; + /** + * @returns a string that will be used as the content of the help dialog + * instead of the one provided by default. + */ + customHelp?: () => string; + /** + * If this provider might want to request to be shown again, provide an ID. + */ + id?: AccessibleViewProviderId; + + /** + * Keybinding items to configure + */ + configureKeybindingItems?: IQuickPickItem[]; + + /** + * Keybinding items that are already configured + */ + configuredKeybindingItems?: IQuickPickItem[]; +} + + +export interface IAccessibleViewContentProvider extends IBasicContentProvider, IDisposable { + id: AccessibleViewProviderId; + verbositySettingKey: string; + /** + * Note that a Codicon class should be provided for each action. + * If not, a default will be used. + */ + onKeyDown?(e: IKeyboardEvent): void; + /** + * When the language is markdown, this is provided by default. + */ + getSymbols?(): IAccessibleViewSymbol[]; + /** + * Note that this will only take effect if the provider has an ID. + */ + onDidRequestClearLastProvider?: Event; +} + + +export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { + markdownToParse?: string; + firstListItem?: string; + lineNumber?: number; + endLineNumber?: number; +} + +export interface IPosition { + lineNumber: number; + column: number; +} + +export interface IAccessibleViewService { + readonly _serviceBrand: undefined; + // The provider will be disposed when the view is closed + show(provider: AccesibleViewContentProvider, position?: IPosition): void; + showLastProvider(id: AccessibleViewProviderId): void; + showAccessibleViewHelp(): void; + next(): void; + previous(): void; + navigateToCodeBlock(type: 'next' | 'previous'): void; + goToSymbol(): void; + disableHint(): void; + getPosition(id: AccessibleViewProviderId): IPosition | undefined; + setPosition(position: IPosition, reveal?: boolean, select?: boolean): void; + getLastPosition(): IPosition | undefined; + /** + * If the setting is enabled, provides the open accessible view hint as a localized string. + * @param verbositySettingKey The setting key for the verbosity of the feature + */ + getOpenAriaHint(verbositySettingKey: string): string | null; + getCodeBlockContext(): ICodeBlockActionContext | undefined; + configureKeybindings(unassigned: boolean): void; + openHelpLink(): void; +} + + +export interface ICodeBlockActionContext { + code: string; + languageId?: string; + codeBlockIndex: number; + element: unknown; +} + +export type AccesibleViewContentProvider = AccessibleContentProvider | ExtensionContentProvider; + +export class AccessibleContentProvider extends Disposable implements IAccessibleViewContentProvider { + + constructor( + public id: AccessibleViewProviderId, + public options: IAccessibleViewOptions, + public provideContent: () => string, + public onClose: () => void, + public verbositySettingKey: string, + public onOpen?: () => void, + public actions?: IAction[], + public provideNextContent?: () => string | undefined, + public providePreviousContent?: () => string | undefined, + public onDidChangeContent?: Event, + public onKeyDown?: (e: IKeyboardEvent) => void, + public getSymbols?: () => IAccessibleViewSymbol[], + public onDidRequestClearLastProvider?: Event, + ) { + super(); + } +} + +export class ExtensionContentProvider extends Disposable implements IBasicContentProvider { + + constructor( + public readonly id: string, + public options: IAccessibleViewOptions, + public provideContent: () => string, + public onClose: () => void, + public onOpen?: () => void, + public provideNextContent?: () => string | undefined, + public providePreviousContent?: () => string | undefined, + public actions?: IAction[], + public onDidChangeContent?: Event, + ) { + super(); + } +} + +export interface IBasicContentProvider extends IDisposable { + id: string; + options: IAccessibleViewOptions; + onClose(): void; + provideContent(): string; + onOpen?(): void; + actions?: IAction[]; + providePreviousContent?(): void; + provideNextContent?(): void; + onDidChangeContent?: Event; +} diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts new file mode 100644 index 00000000000..d915dd4e0f4 --- /dev/null +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/lifecycle'; +import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export interface IAccessibleViewImplentation { + type: AccessibleViewType; + priority: number; + name: string; + /** + * @returns the provider or undefined if the view should not be shown + */ + getProvider: (accessor: ServicesAccessor) => AccessibleContentProvider | ExtensionContentProvider | undefined; + when?: ContextKeyExpression | undefined; +} + +export const AccessibleViewRegistry = new class AccessibleViewRegistry { + _implementations: IAccessibleViewImplentation[] = []; + + register(implementation: IAccessibleViewImplentation): IDisposable { + this._implementations.push(implementation); + return { + dispose: () => { + const idx = this._implementations.indexOf(implementation); + if (idx !== -1) { + this._implementations.splice(idx, 1); + } + } + }; + } + + getImplementations(): IAccessibleViewImplentation[] { + return this._implementations; + } +}; + diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts index d6a89b230f8..3022a90e867 100644 --- a/src/vs/platform/accessibility/common/accessibility.ts +++ b/src/vs/platform/accessibility/common/accessibility.ts @@ -48,3 +48,4 @@ export function isAccessibilityInformation(obj: any): obj is IAccessibilityInfor && (typeof obj.role === 'undefined' || typeof obj.role === 'string'); } +export const ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX = 'ACCESSIBLE_VIEW_SHOWN_'; diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index de4044b74f4..b0e99c5d0f5 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -3,14 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CachedFunction } from 'vs/base/common/cache'; +import { getStructuralKey } from 'vs/base/common/equals'; +import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; +import { derived, observableFromEvent } from 'vs/base/common/observable'; +import { ValueWithChangeEventFromObservable } from 'vs/base/common/observableInternal/utils'; +import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Event } from 'vs/base/common/event'; -import { localize } from 'vs/nls'; -import { observableFromEvent, derived } from 'vs/base/common/observable'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAccessibilitySignalService = createDecorator('accessibilitySignalService'); @@ -19,18 +23,34 @@ export interface IAccessibilitySignalService { readonly _serviceBrand: undefined; playSignal(signal: AccessibilitySignal, options?: IAccessbilitySignalOptions): Promise; playSignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise; - isSoundEnabled(signal: AccessibilitySignal): boolean; - isAnnouncementEnabled(signal: AccessibilitySignal): boolean; - onSoundEnabledChanged(signal: AccessibilitySignal): Event; - onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event; - - playSound(signal: Sound, allowManyInParallel?: boolean): Promise; playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; + + getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent; + getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number; + /** + * Avoid this method and prefer `.playSignal`! + * Only use it when you want to play the sound regardless of enablement, e.g. in the settings quick pick. + */ + playSound(signal: Sound, allowManyInParallel: boolean, token: typeof AcknowledgeDocCommentsToken): Promise; + + /** @deprecated Use getEnabledState(...).onChange */ + isSoundEnabled(signal: AccessibilitySignal): boolean; + /** @deprecated Use getEnabledState(...).value */ + isAnnouncementEnabled(signal: AccessibilitySignal): boolean; + /** @deprecated Use getEnabledState(...).onChange */ + onSoundEnabledChanged(signal: AccessibilitySignal): Event; } +/** Make sure you understand the doc comments of the method you want to call when using this token! */ +export const AcknowledgeDocCommentsToken = Symbol('AcknowledgeDocCommentsToken'); + +export type AccessibilityModality = 'sound' | 'announcement'; + export interface IAccessbilitySignalOptions { allowManyInParallel?: boolean; + modality?: AccessibilityModality; + /** * The source that triggered the signal (e.g. "diffEditor.cursorPositionChanged"). */ @@ -47,7 +67,7 @@ export interface IAccessbilitySignalOptions { export class AccessibilitySignalService extends Disposable implements IAccessibilitySignalService { readonly _serviceBrand: undefined; private readonly sounds: Map = new Map(); - private readonly screenReaderAttached = observableFromEvent( + private readonly screenReaderAttached = observableFromEvent(this, this.accessibilityService.onDidChangeScreenReaderOptimized, () => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized() ); @@ -61,13 +81,19 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi super(); } + public getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent { + return new ValueWithChangeEventFromObservable(this._signalEnabledState.get({ signal, userGesture, modality })); + } + public async playSignal(signal: AccessibilitySignal, options: IAccessbilitySignalOptions = {}): Promise { + const shouldPlayAnnouncement = options.modality === 'announcement' || options.modality === undefined; const announcementMessage = signal.announcementMessage; - if (this.isAnnouncementEnabled(signal, options.userGesture) && announcementMessage) { + if (shouldPlayAnnouncement && this.isAnnouncementEnabled(signal, options.userGesture) && announcementMessage) { this.accessibilityService.status(announcementMessage); } - if (this.isSoundEnabled(signal, options.userGesture)) { + const shouldPlaySound = options.modality === 'sound' || options.modality === undefined; + if (shouldPlaySound && this.isSoundEnabled(signal, options.userGesture)) { this.sendSignalTelemetry(signal, options.source); await this.playSound(signal.sound.getSound(), options.allowManyInParallel); } @@ -119,7 +145,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } private getVolumeInPercent(): number { - const volume = this.configurationService.getValue('accessibilitySignals.volume'); + const volume = this.configurationService.getValue('accessibility.signalOptions.volume'); if (typeof volume !== 'number') { return 50; } @@ -173,83 +199,68 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return toDisposable(() => playing = false); } - private readonly obsoleteAccessibilitySignalsEnabled = observableFromEvent( - Event.filter(this.configurationService.onDidChangeConfiguration, (e) => - e.affectsConfiguration('accessibilitySignals.enabled') - ), - () => /** @description config: accessibilitySignals.enabled */ this.configurationService.getValue<'on' | 'off' | 'auto' | 'userGesture' | 'always' | 'never'>('accessibilitySignals.enabled') - ); + private readonly _signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ + sound: EnabledState; + announcement: EnabledState; + }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); - private readonly isSoundEnabledCache = new Cache((event: { readonly signal: AccessibilitySignal; readonly userGesture?: boolean }) => { - const settingObservable = observableFromEvent( - Event.filter(this.configurationService.onDidChangeConfiguration, (e) => - e.affectsConfiguration(event.signal.legacySoundSettingsKey) || e.affectsConfiguration(event.signal.settingsKey) - ), - () => this.configurationService.getValue<'on' | 'off' | 'auto' | 'userGesture' | 'always' | 'never'>(event.signal.settingsKey + '.sound') - ); - return derived(reader => { - /** @description sound enabled */ - const setting = settingObservable.read(reader); - if ( - setting === 'on' || - (setting === 'auto' && this.screenReaderAttached.read(reader)) - ) { - return true; - } else if (setting === 'always' || setting === 'userGesture' && event.userGesture) { - return true; - } + private readonly _signalEnabledState = new CachedFunction( + { getCacheKey: getStructuralKey }, + (arg: { signal: AccessibilitySignal; userGesture: boolean; modality?: AccessibilityModality | undefined }) => { + return derived(reader => { + /** @description sound enabled */ + const setting = this._signalConfigValue.get(arg.signal).read(reader); - const obsoleteSetting = this.obsoleteAccessibilitySignalsEnabled.read(reader); - if ( - obsoleteSetting === 'on' || - (obsoleteSetting === 'auto' && this.screenReaderAttached.read(reader)) - ) { - return true; - } - - return false; - }); - }, JSON.stringify); - - private readonly isAnnouncementEnabledCache = new Cache((event: { readonly signal: AccessibilitySignal; readonly userGesture?: boolean }) => { - const settingObservable = observableFromEvent( - Event.filter(this.configurationService.onDidChangeConfiguration, (e) => - e.affectsConfiguration(event.signal.legacyAnnouncementSettingsKey!) || e.affectsConfiguration(event.signal.settingsKey) - ), - () => event.signal.announcementMessage ? this.configurationService.getValue<'auto' | 'off' | 'userGesture' | 'always' | 'never'>(event.signal.settingsKey + '.announcement') : false - ); - return derived(reader => { - /** @description announcement enabled */ - const setting = settingObservable.read(reader); - if ( - !this.screenReaderAttached.read(reader) - ) { + if (arg.modality === 'sound' || arg.modality === undefined) { + if (checkEnabledState(setting.sound, () => this.screenReaderAttached.read(reader), arg.userGesture)) { + return true; + } + } + if (arg.modality === 'announcement' || arg.modality === undefined) { + if (checkEnabledState(setting.announcement, () => this.screenReaderAttached.read(reader), arg.userGesture)) { + return true; + } + } return false; - } - return setting === 'auto' || setting === 'always' || setting === 'userGesture' && event.userGesture; - }); - }, JSON.stringify); + }).recomputeInitiallyAndOnChange(this._store); + } + ); public isAnnouncementEnabled(signal: AccessibilitySignal, userGesture?: boolean): boolean { if (!signal.announcementMessage) { return false; } - return this.isAnnouncementEnabledCache.get({ signal, userGesture }).get() ?? false; + return this._signalEnabledState.get({ signal, userGesture: !!userGesture, modality: 'announcement' }).get(); } public isSoundEnabled(signal: AccessibilitySignal, userGesture?: boolean): boolean { - return this.isSoundEnabledCache.get({ signal, userGesture }).get() ?? false; + return this._signalEnabledState.get({ signal, userGesture: !!userGesture, modality: 'sound' }).get(); } public onSoundEnabledChanged(signal: AccessibilitySignal): Event { - return Event.fromObservableLight(this.isSoundEnabledCache.get({ signal })); + return this.getEnabledState(signal, false).onDidChange; } - public onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event { - return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal })); + public getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number { + if (!this.configurationService.getValue('accessibility.signalOptions.debouncePositionChanges')) { + return 0; + } + let value: { sound: number; announcement: number }; + if (signal.name === AccessibilitySignal.errorAtPosition.name && mode === 'positional') { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.errorAtPosition'); + } else if (signal.name === AccessibilitySignal.warningAtPosition.name && mode === 'positional') { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.warningAtPosition'); + } else { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.general'); + } + return modality === 'sound' ? value.sound : value.announcement; } } +type EnabledState = 'on' | 'off' | 'auto' | 'userGesture' | 'always' | 'never'; +function checkEnabledState(state: EnabledState, getScreenReaderAttached: () => boolean, isTriggeredByUserGesture: boolean): boolean { + return state === 'on' || state === 'always' || (state === 'auto' && getScreenReaderAttached()) || state === 'userGesture' && isTriggeredByUserGesture; +} /** * Play the given audio url. @@ -273,23 +284,6 @@ function playAudio(url: string, volume: number): Promise { }); } -class Cache { - private readonly map = new Map(); - constructor(private readonly getValue: (value: TArg) => TValue, private readonly getKey: (value: TArg) => unknown) { - } - - public get(arg: TArg): TValue { - if (this.map.has(arg)) { - return this.map.get(arg)!; - } - - const value = this.getValue(arg); - const key = this.getKey(arg); - this.map.set(key, value); - return value; - } -} - /** * Corresponds to the audio files in ./media. */ @@ -301,6 +295,7 @@ export class Sound { public static readonly error = Sound.register({ fileName: 'error.mp3' }); public static readonly warning = Sound.register({ fileName: 'warning.mp3' }); + public static readonly success = Sound.register({ fileName: 'success.mp3' }); public static readonly foldedArea = Sound.register({ fileName: 'foldedAreas.mp3' }); public static readonly break = Sound.register({ fileName: 'break.mp3' }); public static readonly quickFixes = Sound.register({ fileName: 'quickFixes.mp3' }); @@ -311,7 +306,6 @@ export class Sound { public static readonly diffLineDeleted = Sound.register({ fileName: 'diffLineDeleted.mp3' }); public static readonly diffLineModified = Sound.register({ fileName: 'diffLineModified.mp3' }); public static readonly chatRequestSent = Sound.register({ fileName: 'chatRequestSent.mp3' }); - public static readonly chatResponsePending = Sound.register({ fileName: 'chatResponsePending.mp3' }); public static readonly chatResponseReceived1 = Sound.register({ fileName: 'chatResponseReceived1.mp3' }); public static readonly chatResponseReceived2 = Sound.register({ fileName: 'chatResponseReceived2.mp3' }); public static readonly chatResponseReceived3 = Sound.register({ fileName: 'chatResponseReceived3.mp3' }); @@ -321,6 +315,7 @@ export class Sound { public static readonly format = Sound.register({ fileName: 'format.mp3' }); public static readonly voiceRecordingStarted = Sound.register({ fileName: 'voiceRecordingStarted.mp3' }); public static readonly voiceRecordingStopped = Sound.register({ fileName: 'voiceRecordingStopped.mp3' }); + public static readonly progress = Sound.register({ fileName: 'progress.mp3' }); private constructor(public readonly fileName: string) { } } @@ -340,30 +335,16 @@ export class SoundSource { } } -export const enum AccessibilityAlertSettingId { - Save = 'accessibility.alert.save', - Format = 'accessibility.alert.format', - Clear = 'accessibility.alert.clear', - Breakpoint = 'accessibility.alert.breakpoint', - Error = 'accessibility.alert.error', - Warning = 'accessibility.alert.warning', - FoldedArea = 'accessibility.alert.foldedArea', - TerminalQuickFix = 'accessibility.alert.terminalQuickFix', - TerminalBell = 'accessibility.alert.terminalBell', - TerminalCommandFailed = 'accessibility.alert.terminalCommandFailed', - TaskCompleted = 'accessibility.alert.taskCompleted', - TaskFailed = 'accessibility.alert.taskFailed', - ChatRequestSent = 'accessibility.alert.chatRequestSent', - NotebookCellCompleted = 'accessibility.alert.notebookCellCompleted', - NotebookCellFailed = 'accessibility.alert.notebookCellFailed', - OnDebugBreak = 'accessibility.alert.onDebugBreak', - NoInlayHints = 'accessibility.alert.noInlayHints', - LineHasBreakpoint = 'accessibility.alert.lineHasBreakpoint', - ChatResponsePending = 'accessibility.alert.chatResponsePending' -} - - export class AccessibilitySignal { + private constructor( + public readonly sound: SoundSource, + public readonly name: string, + public readonly legacySoundSettingsKey: string | undefined, + public readonly settingsKey: string, + public readonly legacyAnnouncementSettingsKey: string | undefined, + public readonly announcementMessage: string | undefined + ) { } + private static _signals = new Set(); private static register(options: { name: string; @@ -374,13 +355,21 @@ export class AccessibilitySignal { */ randomOneOf: Sound[]; }; - legacySoundSettingsKey: string; + legacySoundSettingsKey?: string; settingsKey: string; - legacyAnnouncementSettingsKey?: AccessibilityAlertSettingId; + legacyAnnouncementSettingsKey?: string; announcementMessage?: string; + delaySettingsKey?: string; }): AccessibilitySignal { const soundSource = new SoundSource('randomOneOf' in options.sound ? options.sound.randomOneOf : [options.sound]); - const signal = new AccessibilitySignal(soundSource, options.name, options.legacySoundSettingsKey, options.settingsKey, options.legacyAnnouncementSettingsKey, options.announcementMessage); + const signal = new AccessibilitySignal( + soundSource, + options.name, + options.legacySoundSettingsKey, + options.settingsKey, + options.legacyAnnouncementSettingsKey, + options.announcementMessage, + ); AccessibilitySignal._signals.add(signal); return signal; } @@ -389,154 +378,177 @@ export class AccessibilitySignal { return [...this._signals]; } - public static readonly error = AccessibilitySignal.register({ + public static readonly errorAtPosition = AccessibilitySignal.register({ + name: localize('accessibilitySignals.positionHasError.name', 'Error at Position'), + sound: Sound.error, + announcementMessage: localize('accessibility.signals.positionHasError', 'Error'), + settingsKey: 'accessibility.signals.positionHasError', + delaySettingsKey: 'accessibility.signalOptions.delays.errorAtPosition' + }); + public static readonly warningAtPosition = AccessibilitySignal.register({ + name: localize('accessibilitySignals.positionHasWarning.name', 'Warning at Position'), + sound: Sound.warning, + announcementMessage: localize('accessibility.signals.positionHasWarning', 'Warning'), + settingsKey: 'accessibility.signals.positionHasWarning', + delaySettingsKey: 'accessibility.signalOptions.delays.warningAtPosition' + }); + + public static readonly errorOnLine = AccessibilitySignal.register({ name: localize('accessibilitySignals.lineHasError.name', 'Error on Line'), sound: Sound.error, legacySoundSettingsKey: 'audioCues.lineHasError', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.Error, - announcementMessage: localize('accessibility.signals.lineHasError', 'Error'), - settingsKey: 'accessibility.signals.lineHasError' + legacyAnnouncementSettingsKey: 'accessibility.alert.error', + announcementMessage: localize('accessibility.signals.lineHasError', 'Error on Line'), + settingsKey: 'accessibility.signals.lineHasError', }); - public static readonly warning = AccessibilitySignal.register({ + + public static readonly warningOnLine = AccessibilitySignal.register({ name: localize('accessibilitySignals.lineHasWarning.name', 'Warning on Line'), sound: Sound.warning, legacySoundSettingsKey: 'audioCues.lineHasWarning', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.Warning, - announcementMessage: localize('accessibility.signals.lineHasWarning', 'Warning'), - settingsKey: 'accessibility.signals.lineHasWarning' + legacyAnnouncementSettingsKey: 'accessibility.alert.warning', + announcementMessage: localize('accessibility.signals.lineHasWarning', 'Warning on Line'), + settingsKey: 'accessibility.signals.lineHasWarning', }); public static readonly foldedArea = AccessibilitySignal.register({ name: localize('accessibilitySignals.lineHasFoldedArea.name', 'Folded Area on Line'), sound: Sound.foldedArea, legacySoundSettingsKey: 'audioCues.lineHasFoldedArea', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.FoldedArea, + legacyAnnouncementSettingsKey: 'accessibility.alert.foldedArea', announcementMessage: localize('accessibility.signals.lineHasFoldedArea', 'Folded'), - settingsKey: 'accessibility.signals.lineHasFoldedArea' + settingsKey: 'accessibility.signals.lineHasFoldedArea', }); public static readonly break = AccessibilitySignal.register({ name: localize('accessibilitySignals.lineHasBreakpoint.name', 'Breakpoint on Line'), sound: Sound.break, legacySoundSettingsKey: 'audioCues.lineHasBreakpoint', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.Breakpoint, + legacyAnnouncementSettingsKey: 'accessibility.alert.breakpoint', announcementMessage: localize('accessibility.signals.lineHasBreakpoint', 'Breakpoint'), - settingsKey: 'accessibility.signals.lineHasBreakpoint' + settingsKey: 'accessibility.signals.lineHasBreakpoint', }); public static readonly inlineSuggestion = AccessibilitySignal.register({ name: localize('accessibilitySignals.lineHasInlineSuggestion.name', 'Inline Suggestion on Line'), sound: Sound.quickFixes, legacySoundSettingsKey: 'audioCues.lineHasInlineSuggestion', - settingsKey: 'accessibility.signals.lineHasInlineSuggestion' + settingsKey: 'accessibility.signals.lineHasInlineSuggestion', }); public static readonly terminalQuickFix = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalQuickFix.name', 'Terminal Quick Fix'), sound: Sound.quickFixes, legacySoundSettingsKey: 'audioCues.terminalQuickFix', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.TerminalQuickFix, + legacyAnnouncementSettingsKey: 'accessibility.alert.terminalQuickFix', announcementMessage: localize('accessibility.signals.terminalQuickFix', 'Quick Fix'), - settingsKey: 'accessibility.signals.terminalQuickFix' + settingsKey: 'accessibility.signals.terminalQuickFix', }); public static readonly onDebugBreak = AccessibilitySignal.register({ name: localize('accessibilitySignals.onDebugBreak.name', 'Debugger Stopped on Breakpoint'), sound: Sound.break, legacySoundSettingsKey: 'audioCues.onDebugBreak', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.OnDebugBreak, + legacyAnnouncementSettingsKey: 'accessibility.alert.onDebugBreak', announcementMessage: localize('accessibility.signals.onDebugBreak', 'Breakpoint'), - settingsKey: 'accessibility.signals.onDebugBreak' + settingsKey: 'accessibility.signals.onDebugBreak', }); public static readonly noInlayHints = AccessibilitySignal.register({ name: localize('accessibilitySignals.noInlayHints', 'No Inlay Hints on Line'), sound: Sound.error, legacySoundSettingsKey: 'audioCues.noInlayHints', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.NoInlayHints, + legacyAnnouncementSettingsKey: 'accessibility.alert.noInlayHints', announcementMessage: localize('accessibility.signals.noInlayHints', 'No Inlay Hints'), - settingsKey: 'accessibility.signals.noInlayHints' + settingsKey: 'accessibility.signals.noInlayHints', }); public static readonly taskCompleted = AccessibilitySignal.register({ name: localize('accessibilitySignals.taskCompleted', 'Task Completed'), sound: Sound.taskCompleted, legacySoundSettingsKey: 'audioCues.taskCompleted', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.TaskCompleted, + legacyAnnouncementSettingsKey: 'accessibility.alert.taskCompleted', announcementMessage: localize('accessibility.signals.taskCompleted', 'Task Completed'), - settingsKey: 'accessibility.signals.taskCompleted' + settingsKey: 'accessibility.signals.taskCompleted', }); public static readonly taskFailed = AccessibilitySignal.register({ name: localize('accessibilitySignals.taskFailed', 'Task Failed'), sound: Sound.taskFailed, legacySoundSettingsKey: 'audioCues.taskFailed', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.TaskFailed, + legacyAnnouncementSettingsKey: 'accessibility.alert.taskFailed', announcementMessage: localize('accessibility.signals.taskFailed', 'Task Failed'), - settingsKey: 'accessibility.signals.taskFailed' + settingsKey: 'accessibility.signals.taskFailed', }); public static readonly terminalCommandFailed = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalCommandFailed', 'Terminal Command Failed'), sound: Sound.error, legacySoundSettingsKey: 'audioCues.terminalCommandFailed', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.TerminalCommandFailed, + legacyAnnouncementSettingsKey: 'accessibility.alert.terminalCommandFailed', announcementMessage: localize('accessibility.signals.terminalCommandFailed', 'Command Failed'), - settingsKey: 'accessibility.signals.terminalCommandFailed' + settingsKey: 'accessibility.signals.terminalCommandFailed', + }); + + public static readonly terminalCommandSucceeded = AccessibilitySignal.register({ + name: localize('accessibilitySignals.terminalCommandSucceeded', 'Terminal Command Succeeded'), + sound: Sound.success, + announcementMessage: localize('accessibility.signals.terminalCommandSucceeded', 'Command Succeeded'), + settingsKey: 'accessibility.signals.terminalCommandSucceeded', }); public static readonly terminalBell = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalBell', 'Terminal Bell'), sound: Sound.terminalBell, legacySoundSettingsKey: 'audioCues.terminalBell', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.TerminalBell, + legacyAnnouncementSettingsKey: 'accessibility.alert.terminalBell', announcementMessage: localize('accessibility.signals.terminalBell', 'Terminal Bell'), - settingsKey: 'accessibility.signals.terminalBell' + settingsKey: 'accessibility.signals.terminalBell', }); public static readonly notebookCellCompleted = AccessibilitySignal.register({ name: localize('accessibilitySignals.notebookCellCompleted', 'Notebook Cell Completed'), sound: Sound.taskCompleted, legacySoundSettingsKey: 'audioCues.notebookCellCompleted', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.NotebookCellCompleted, + legacyAnnouncementSettingsKey: 'accessibility.alert.notebookCellCompleted', announcementMessage: localize('accessibility.signals.notebookCellCompleted', 'Notebook Cell Completed'), - settingsKey: 'accessibility.signals.notebookCellCompleted' + settingsKey: 'accessibility.signals.notebookCellCompleted', }); public static readonly notebookCellFailed = AccessibilitySignal.register({ name: localize('accessibilitySignals.notebookCellFailed', 'Notebook Cell Failed'), sound: Sound.taskFailed, legacySoundSettingsKey: 'audioCues.notebookCellFailed', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.NotebookCellFailed, + legacyAnnouncementSettingsKey: 'accessibility.alert.notebookCellFailed', announcementMessage: localize('accessibility.signals.notebookCellFailed', 'Notebook Cell Failed'), - settingsKey: 'accessibility.signals.notebookCellFailed' + settingsKey: 'accessibility.signals.notebookCellFailed', }); public static readonly diffLineInserted = AccessibilitySignal.register({ name: localize('accessibilitySignals.diffLineInserted', 'Diff Line Inserted'), sound: Sound.diffLineInserted, legacySoundSettingsKey: 'audioCues.diffLineInserted', - settingsKey: 'accessibility.signals.diffLineInserted' + settingsKey: 'accessibility.signals.diffLineInserted', }); public static readonly diffLineDeleted = AccessibilitySignal.register({ name: localize('accessibilitySignals.diffLineDeleted', 'Diff Line Deleted'), sound: Sound.diffLineDeleted, legacySoundSettingsKey: 'audioCues.diffLineDeleted', - settingsKey: 'accessibility.signals.diffLineDeleted' + settingsKey: 'accessibility.signals.diffLineDeleted', }); public static readonly diffLineModified = AccessibilitySignal.register({ name: localize('accessibilitySignals.diffLineModified', 'Diff Line Modified'), sound: Sound.diffLineModified, legacySoundSettingsKey: 'audioCues.diffLineModified', - settingsKey: 'accessibility.signals.diffLineModified' + settingsKey: 'accessibility.signals.diffLineModified', }); public static readonly chatRequestSent = AccessibilitySignal.register({ name: localize('accessibilitySignals.chatRequestSent', 'Chat Request Sent'), sound: Sound.chatRequestSent, legacySoundSettingsKey: 'audioCues.chatRequestSent', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.ChatRequestSent, + legacyAnnouncementSettingsKey: 'accessibility.alert.chatRequestSent', announcementMessage: localize('accessibility.signals.chatRequestSent', 'Chat Request Sent'), - settingsKey: 'accessibility.signals.chatRequestSent' + settingsKey: 'accessibility.signals.chatRequestSent', }); public static readonly chatResponseReceived = AccessibilitySignal.register({ @@ -553,20 +565,20 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.chatResponseReceived' }); - public static readonly chatResponsePending = AccessibilitySignal.register({ - name: localize('accessibilitySignals.chatResponsePending', 'Chat Response Pending'), - sound: Sound.chatResponsePending, + public static readonly progress = AccessibilitySignal.register({ + name: localize('accessibilitySignals.progress', 'Progress'), + sound: Sound.progress, legacySoundSettingsKey: 'audioCues.chatResponsePending', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.ChatResponsePending, - announcementMessage: localize('accessibility.signals.chatResponsePending', 'Chat Response Pending'), - settingsKey: 'accessibility.signals.chatResponsePending' + legacyAnnouncementSettingsKey: 'accessibility.alert.progress', + announcementMessage: localize('accessibility.signals.progress', 'Progress'), + settingsKey: 'accessibility.signals.progress' }); public static readonly clear = AccessibilitySignal.register({ name: localize('accessibilitySignals.clear', 'Clear'), sound: Sound.clear, legacySoundSettingsKey: 'audioCues.clear', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.Clear, + legacyAnnouncementSettingsKey: 'accessibility.alert.clear', announcementMessage: localize('accessibility.signals.clear', 'Clear'), settingsKey: 'accessibility.signals.clear' }); @@ -575,7 +587,7 @@ export class AccessibilitySignal { name: localize('accessibilitySignals.save', 'Save'), sound: Sound.save, legacySoundSettingsKey: 'audioCues.save', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.Save, + legacyAnnouncementSettingsKey: 'accessibility.alert.save', announcementMessage: localize('accessibility.signals.save', 'Save'), settingsKey: 'accessibility.signals.save' }); @@ -584,7 +596,7 @@ export class AccessibilitySignal { name: localize('accessibilitySignals.format', 'Format'), sound: Sound.format, legacySoundSettingsKey: 'audioCues.format', - legacyAnnouncementSettingsKey: AccessibilityAlertSettingId.Format, + legacyAnnouncementSettingsKey: 'accessibility.alert.format', announcementMessage: localize('accessibility.signals.format', 'Format'), settingsKey: 'accessibility.signals.format' }); @@ -602,13 +614,5 @@ export class AccessibilitySignal { legacySoundSettingsKey: 'audioCues.voiceRecordingStopped', settingsKey: 'accessibility.signals.voiceRecordingStopped' }); - - private constructor( - public readonly sound: SoundSource, - public readonly name: string, - public readonly legacySoundSettingsKey: string, - public readonly settingsKey: string, - public readonly legacyAnnouncementSettingsKey?: string, - public readonly announcementMessage?: string, - ) { } } + diff --git a/src/vs/platform/accessibilitySignal/browser/media/chatResponsePending.mp3 b/src/vs/platform/accessibilitySignal/browser/media/progress.mp3 similarity index 100% rename from src/vs/platform/accessibilitySignal/browser/media/chatResponsePending.mp3 rename to src/vs/platform/accessibilitySignal/browser/media/progress.mp3 diff --git a/src/vs/platform/accessibilitySignal/browser/media/success.mp3 b/src/vs/platform/accessibilitySignal/browser/media/success.mp3 new file mode 100644 index 00000000000..dee1d5061e4 Binary files /dev/null and b/src/vs/platform/accessibilitySignal/browser/media/success.mp3 differ diff --git a/src/vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler.ts b/src/vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler.ts new file mode 100644 index 00000000000..faa617f4566 --- /dev/null +++ b/src/vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/async'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; + +const PROGRESS_SIGNAL_LOOP_DELAY = 5000; + +/** + * Schedules a signal to play while progress is happening. + */ +export class AccessibilityProgressSignalScheduler extends Disposable { + private _scheduler: RunOnceScheduler; + private _signalLoop: IDisposable | undefined; + constructor(msDelayTime: number, msLoopTime: number | undefined, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { + super(); + this._scheduler = new RunOnceScheduler(() => { + this._signalLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.progress, msLoopTime ?? PROGRESS_SIGNAL_LOOP_DELAY); + }, msDelayTime); + this._scheduler.schedule(); + } + override dispose(): void { + super.dispose(); + this._signalLoop?.dispose(); + this._scheduler.dispose(); + } +} + diff --git a/src/vs/platform/action/common/action.ts b/src/vs/platform/action/common/action.ts index 8b67550ed21..c0f4c2dd053 100644 --- a/src/vs/platform/action/common/action.ts +++ b/src/vs/platform/action/common/action.ts @@ -85,6 +85,10 @@ export interface ICommandAction { tooltip?: string | ILocalizedString; icon?: Icon; source?: ICommandActionSource; + /** + * Precondition controls enablement (for example for a menu item, show + * it in grey or for a command, do not allow to invoke it) + */ precondition?: ContextKeyExpression; /** diff --git a/src/vs/platform/actionWidget/browser/actionWidget.ts b/src/vs/platform/actionWidget/browser/actionWidget.ts index d9fb3fbb3f6..20a030b67ab 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.ts +++ b/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -21,7 +21,7 @@ import { inputActiveOptionBackground, registerColor } from 'vs/platform/theme/co registerColor( 'actionBar.toggledBackground', - { dark: inputActiveOptionBackground, light: inputActiveOptionBackground, hcDark: inputActiveOptionBackground, hcLight: inputActiveOptionBackground, }, + inputActiveOptionBackground, localize('actionBar.toggledBackground', 'Background color for toggled action items in action bar.') ); @@ -36,7 +36,7 @@ export interface IActionWidgetService { show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void; - hide(): void; + hide(didCancel?: boolean): void; readonly isVisible: boolean; } @@ -87,8 +87,8 @@ class ActionWidgetService extends Disposable implements IActionWidgetService { this._list?.value?.focusNext(); } - hide() { - this._list.value?.hide(); + hide(didCancel?: boolean) { + this._list.value?.hide(didCancel); this._list.clear(); } @@ -139,7 +139,7 @@ class ActionWidgetService extends Disposable implements IActionWidgetService { widget.style.width = `${width}px`; const focusTracker = renderDisposables.add(dom.trackFocus(element)); - renderDisposables.add(focusTracker.onDidBlur(() => this.hide())); + renderDisposables.add(focusTracker.onDidBlur(() => this.hide(true))); return renderDisposables; } @@ -179,7 +179,7 @@ registerAction2(class extends Action2 { } run(accessor: ServicesAccessor): void { - accessor.get(IActionWidgetService).hide(); + accessor.get(IActionWidgetService).hide(true); } }); diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index 79cbe6faa37..530b1fe6170 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -5,15 +5,18 @@ import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ActionRunner, IAction, IActionRunner, SubmenuAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; -import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IToolBarRenderOptions } from 'vs/platform/actions/browser/toolbar'; +import { MenuId, IMenuService, MenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -44,6 +47,7 @@ export class WorkbenchButtonBar extends ButtonBar { @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, + @IHoverService private readonly _hoverService: IHoverService, ) { super(container); @@ -65,7 +69,7 @@ export class WorkbenchButtonBar extends ButtonBar { super.dispose(); } - update(actions: IAction[]): void { + update(actions: IAction[], secondary: IAction[]): void { const conifgProvider: IButtonConfigProvider = this._options?.buttonConfigProvider ?? (() => ({ showLabel: true })); @@ -121,28 +125,59 @@ export class WorkbenchButtonBar extends ButtonBar { } else { tooltip = action.label; } - this._updateStore.add(setupCustomHover(hoverDelegate, btn.element, tooltip)); + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, tooltip)); this._updateStore.add(btn.onDidClick(async () => { this._actionRunner.run(action); })); } + + if (secondary.length > 0) { + + const btn = this.addButton({ + secondary: true, + ariaLabel: localize('moreActions', "More Actions") + }); + + btn.icon = Codicon.dropDownButton; + btn.element.classList.add('default-colors', 'monaco-text-button'); + + btn.enabled = true; + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, localize('moreActions', "More Actions"))); + this._updateStore.add(btn.onDidClick(async () => { + this._contextMenuService.showContextMenu({ + getAnchor: () => btn.element, + getActions: () => secondary, + actionRunner: this._actionRunner, + onHide: () => btn.element.setAttribute('aria-expanded', 'false') + }); + btn.element.setAttribute('aria-expanded', 'true'); + + })); + } this._onDidChange.fire(this); } } +export interface IMenuWorkbenchButtonBarOptions extends IWorkbenchButtonBarOptions { + menuOptions?: IMenuActionOptions; + + toolbarOptions?: IToolBarRenderOptions; +} + export class MenuWorkbenchButtonBar extends WorkbenchButtonBar { constructor( container: HTMLElement, menuId: MenuId, - options: IWorkbenchButtonBarOptions | undefined, + options: IMenuWorkbenchButtonBarOptions | undefined, @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, + @IHoverService hoverService: IHoverService, ) { - super(container, options, contextMenuService, keybindingService, telemetryService); + super(container, options, contextMenuService, keybindingService, telemetryService, hoverService); const menu = menuService.createMenu(menuId, contextKeyService); this._store.add(menu); @@ -151,12 +186,16 @@ export class MenuWorkbenchButtonBar extends WorkbenchButtonBar { this.clear(); - const actions = menu - .getActions({ renderShortTitle: true }) - .flatMap(entry => entry[1]); - - super.update(actions); + const primary: IAction[] = []; + const secondary: IAction[] = []; + createAndFillInActionBarActions( + menu, + options?.menuOptions, + { primary, secondary }, + options?.toolbarOptions?.primaryGroup + ); + super.update(primary, secondary); }; this._store.add(menu.onDidChange(update)); update(); diff --git a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index 13ddcb79d8a..69883110543 100644 --- a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts +++ b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -25,6 +25,7 @@ export interface IDropdownWithPrimaryActionViewItemOptions { actionRunner?: IActionRunner; getKeyBinding?: (action: IAction) => ResolvedKeybinding | undefined; hoverDelegate?: IHoverDelegate; + menuAsChild?: boolean; } export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { @@ -57,7 +58,7 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { } this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { - menuAsChild: true, + menuAsChild: _options?.menuAsChild ?? true, classNames: className ? ['codicon', 'codicon-chevron-down', className] : ['codicon', 'codicon-chevron-down'], actionRunner: this._options?.actionRunner, keybindingProvider: this._options?.getKeyBinding, diff --git a/src/vs/platform/actions/browser/floatingMenu.ts b/src/vs/platform/actions/browser/floatingMenu.ts index e6840b10e10..e7285146aa0 100644 --- a/src/vs/platform/actions/browser/floatingMenu.ts +++ b/src/vs/platform/actions/browser/floatingMenu.ts @@ -119,7 +119,7 @@ export class FloatingClickMenu extends AbstractFloatingClickMenu { const w = this.instantiationService.createInstance(FloatingClickWidget, action.label); const node = w.getDomNode(); this.options.container.appendChild(node); - disposable.add(toDisposable(() => this.options.container.removeChild(node))); + disposable.add(toDisposable(() => node.remove())); return w; } diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.css b/src/vs/platform/actions/browser/menuEntryActionViewItem.css index c5cba140485..7eb35af7e4b 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.css +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.css @@ -11,6 +11,20 @@ background-size: 16px; } +.monaco-action-bar .action-item.menu-entry.text-only .action-label { + color: var(--vscode-descriptionForeground); + overflow: hidden; + border-radius: 2px; +} + +.monaco-action-bar .action-item.menu-entry.text-only.use-comma:not(:last-of-type) .action-label::after { + content: ', '; +} + +.monaco-action-bar .action-item.menu-entry.text-only + .action-item:not(.text-only) > .monaco-dropdown .action-label { + color: var(--vscode-descriptionForeground); +} + .monaco-dropdown-with-default { display: flex !important; flex-direction: row; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index da596a8fc6c..58904af44aa 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -31,9 +31,25 @@ import { assertType } from 'vs/base/common/types'; import { asCssVariable, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; + +export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void; +export function createAndFillInContextMenuActions(menu: [string, Array][], target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void; +export function createAndFillInContextMenuActions(menu: IMenu | [string, Array][], optionsOrTarget: IMenuActionOptions | undefined | IAction[] | { primary: IAction[]; secondary: IAction[] }, targetOrPrimaryGroup?: IAction[] | { primary: IAction[]; secondary: IAction[] } | string, primaryGroupOrUndefined?: string): void { + let target: IAction[] | { primary: IAction[]; secondary: IAction[] }; + let primaryGroup: string | ((actionGroup: string) => boolean) | undefined; + let groups: [string, Array][]; + if (Array.isArray(menu)) { + groups = menu; + target = optionsOrTarget as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = targetOrPrimaryGroup as string | undefined; + } else { + const options: IMenuActionOptions | undefined = optionsOrTarget as IMenuActionOptions | undefined; + groups = menu.getActions(options); + target = targetOrPrimaryGroup as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = primaryGroupOrUndefined; + } -export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void { - const groups = menu.getActions(options); const modifierKeyEmitter = ModifierKeyEmitter.getInstance(); const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey); fillInActions(groups, target, useAlternativeActions, primaryGroup ? actionGroup => actionGroup === primaryGroup : actionGroup => actionGroup === 'navigation'); @@ -46,8 +62,42 @@ export function createAndFillInActionBarActions( primaryGroup?: string | ((actionGroup: string) => boolean), shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean +): void; +export function createAndFillInActionBarActions( + menu: [string, Array][], + target: IAction[] | { primary: IAction[]; secondary: IAction[] }, + primaryGroup?: string | ((actionGroup: string) => boolean), + shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, + useSeparatorsInPrimaryActions?: boolean +): void; +export function createAndFillInActionBarActions( + menu: IMenu | [string, Array][], + optionsOrTarget: IMenuActionOptions | undefined | IAction[] | { primary: IAction[]; secondary: IAction[] }, + targetOrPrimaryGroup?: IAction[] | { primary: IAction[]; secondary: IAction[] } | string | ((actionGroup: string) => boolean), + primaryGroupOrShouldInlineSubmenu?: string | ((actionGroup: string) => boolean) | ((action: SubmenuAction, group: string, groupSize: number) => boolean), + shouldInlineSubmenuOrUseSeparatorsInPrimaryActions?: ((action: SubmenuAction, group: string, groupSize: number) => boolean) | boolean, + useSeparatorsInPrimaryActionsOrUndefined?: boolean ): void { - const groups = menu.getActions(options); + let target: IAction[] | { primary: IAction[]; secondary: IAction[] }; + let primaryGroup: string | ((actionGroup: string) => boolean) | undefined; + let shouldInlineSubmenu: ((action: SubmenuAction, group: string, groupSize: number) => boolean) | undefined; + let useSeparatorsInPrimaryActions: boolean | undefined; + let groups: [string, Array][]; + if (Array.isArray(menu)) { + groups = menu; + target = optionsOrTarget as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = targetOrPrimaryGroup as string | ((actionGroup: string) => boolean) | undefined; + shouldInlineSubmenu = primaryGroupOrShouldInlineSubmenu as (action: SubmenuAction, group: string, groupSize: number) => boolean; + useSeparatorsInPrimaryActions = shouldInlineSubmenuOrUseSeparatorsInPrimaryActions as boolean | undefined; + } else { + const options: IMenuActionOptions | undefined = optionsOrTarget as IMenuActionOptions | undefined; + groups = menu.getActions(options); + target = targetOrPrimaryGroup as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = primaryGroupOrShouldInlineSubmenu as string | ((actionGroup: string) => boolean) | undefined; + shouldInlineSubmenu = shouldInlineSubmenuOrUseSeparatorsInPrimaryActions as (action: SubmenuAction, group: string, groupSize: number) => boolean; + useSeparatorsInPrimaryActions = useSeparatorsInPrimaryActionsOrUndefined; + } + const isPrimaryAction = typeof primaryGroup === 'string' ? (actionGroup: string) => actionGroup === primaryGroup : primaryGroup; // Action bars handle alternative actions on their own so the alternative actions should be ignored @@ -121,7 +171,7 @@ export interface IMenuEntryActionViewItemOptions { hoverDelegate?: IHoverDelegate; } -export class MenuEntryActionViewItem extends ActionViewItem { +export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; private readonly _itemClassDispose = this._register(new MutableDisposable()); @@ -129,7 +179,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { constructor( action: MenuItemAction, - options: IMenuEntryActionViewItemOptions | undefined, + protected _options: T | undefined, @IKeybindingService protected readonly _keybindingService: IKeybindingService, @INotificationService protected _notificationService: INotificationService, @IContextKeyService protected _contextKeyService: IContextKeyService, @@ -137,7 +187,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { @IContextMenuService protected _contextMenuService: IContextMenuService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { - super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding, hoverDelegate: options?.hoverDelegate }); + super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: _options?.draggable, keybinding: _options?.keybinding, hoverDelegate: _options?.hoverDelegate }); this._altKey = ModifierKeyEmitter.getInstance(); } @@ -285,6 +335,45 @@ export class MenuEntryActionViewItem extends ActionViewItem { } } +export interface ITextOnlyMenuEntryActionViewItemOptions extends IMenuEntryActionViewItemOptions { + conversational?: boolean; + useComma?: boolean; +} + +export class TextOnlyMenuEntryActionViewItem extends MenuEntryActionViewItem { + + override render(container: HTMLElement): void { + this.options.label = true; + this.options.icon = false; + super.render(container); + container.classList.add('text-only'); + container.classList.toggle('use-comma', this._options?.useComma ?? false); + } + + protected override updateLabel() { + const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService); + if (!kb) { + return super.updateLabel(); + } + if (this.label) { + const kb2 = TextOnlyMenuEntryActionViewItem._symbolPrintEnter(kb); + + if (this._options?.conversational) { + this.label.textContent = localize({ key: 'content2', comment: ['A label with keybindg like "ESC to dismiss"'] }, '{1} to {0}', this._action.label, kb2); + + } else { + this.label.textContent = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', this._action.label, kb2); + } + } + } + + private static _symbolPrintEnter(kb: ResolvedKeybinding) { + return kb.getLabel() + ?.replace(/\benter\b/gi, '\u23CE') + .replace(/\bEscape\b/gi, 'Esc'); + } +} + export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { constructor( diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index a9061e12c94..003ee6e781c 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -5,7 +5,7 @@ import { addDisposableListener, getWindow } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IToolBarOptions, ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IToolBarOptions, ToggleMenuAction, ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction, Separator, SubmenuAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { coalesceInPlace } from 'vs/base/common/arrays'; import { intersection } from 'vs/base/common/collections'; @@ -16,6 +16,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuActionOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { createConfigureKeybindingAction } from 'vs/platform/actions/common/menuService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -90,12 +92,13 @@ export class WorkbenchToolBar extends ToolBar { @IMenuService private readonly _menuService: IMenuService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @IKeybindingService keybindingService: IKeybindingService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ICommandService private readonly _commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, ) { super(container, _contextMenuService, { // defaults - getKeyBinding: (action) => keybindingService.lookupKeybinding(action.id) ?? undefined, + getKeyBinding: (action) => _keybindingService.lookupKeybinding(action.id) ?? undefined, // options (override defaults) ..._options, // mandatory (overide options) @@ -181,8 +184,8 @@ export class WorkbenchToolBar extends ToolBar { coalesceInPlace(extraSecondary); super.setActions(primary, Separator.join(extraSecondary, secondary)); - // add context menu for toggle actions - if (toggleActions.length > 0) { + // add context menu for toggle and configure keybinding actions + if (toggleActions.length > 0 || primary.length > 0) { this._sessionDisposables.add(addDisposableListener(this.getElement(), 'contextmenu', e => { const event = new StandardMouseEvent(getWindow(this.getElement()), e); @@ -193,45 +196,58 @@ export class WorkbenchToolBar extends ToolBar { event.preventDefault(); event.stopPropagation(); - let noHide = false; + const primaryActions = []; - // last item cannot be hidden when using ignore strategy - if (toggleActionsCheckedCount === 1 && this._options?.hiddenItemStrategy === HiddenItemStrategy.Ignore) { - noHide = true; - for (let i = 0; i < toggleActions.length; i++) { - if (toggleActions[i].checked) { - toggleActions[i] = toAction({ - id: action.id, - label: action.label, - checked: true, - enabled: false, - run() { } - }); - break; // there is only one + // -- Configure Keybinding Action -- + if (action instanceof MenuItemAction && action.menuKeybinding) { + primaryActions.push(action.menuKeybinding); + } else if (!(action instanceof SubmenuItemAction || action instanceof ToggleMenuAction)) { + // only enable the configure keybinding action for actions that support keybindings + const supportsKeybindings = !!this._keybindingService.lookupKeybinding(action.id); + primaryActions.push(createConfigureKeybindingAction(this._commandService, this._keybindingService, action.id, undefined, supportsKeybindings)); + } + + // -- Hide Actions -- + if (toggleActions.length > 0) { + let noHide = false; + + // last item cannot be hidden when using ignore strategy + if (toggleActionsCheckedCount === 1 && this._options?.hiddenItemStrategy === HiddenItemStrategy.Ignore) { + noHide = true; + for (let i = 0; i < toggleActions.length; i++) { + if (toggleActions[i].checked) { + toggleActions[i] = toAction({ + id: action.id, + label: action.label, + checked: true, + enabled: false, + run() { } + }); + break; // there is only one + } } } - } - // add "hide foo" actions - let hideAction: IAction; - if (!noHide && (action instanceof MenuItemAction || action instanceof SubmenuItemAction)) { - if (!action.hideActions) { - // no context menu for MenuItemAction instances that support no hiding - // those are fake actions and need to be cleaned up - return; + // add "hide foo" actions + if (!noHide && (action instanceof MenuItemAction || action instanceof SubmenuItemAction)) { + if (!action.hideActions) { + // no context menu for MenuItemAction instances that support no hiding + // those are fake actions and need to be cleaned up + return; + } + primaryActions.push(action.hideActions.hide); + + } else { + primaryActions.push(toAction({ + id: 'label', + label: localize('hide', "Hide"), + enabled: false, + run() { } + })); } - hideAction = action.hideActions.hide; - - } else { - hideAction = toAction({ - id: 'label', - label: localize('hide', "Hide"), - enabled: false, - run() { } - }); } - const actions = Separator.join([hideAction], toggleActions); + const actions = Separator.join(primaryActions, toggleActions); // add "Reset Menu" action if (this._options?.resetMenu && !menuIds) { @@ -246,6 +262,10 @@ export class WorkbenchToolBar extends ToolBar { })); } + if (actions.length === 0) { + return; + } + this._contextMenuService.showContextMenu({ getAnchor: () => event, getActions: () => actions, @@ -313,9 +333,10 @@ export class MenuWorkbenchToolBar extends WorkbenchToolBar { @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, + @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(container, { resetMenu: menuId, ...options }, menuService, contextKeyService, contextMenuService, keybindingService, telemetryService); + super(container, { resetMenu: menuId, ...options }, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); // update logic const menu = this._store.add(menuService.createMenu(menuId, contextKeyService, { emitEventsForSubmenuChanges: true })); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index f21b306d9ed..5bb61d3872b 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -6,7 +6,7 @@ import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { ThemeIcon } from 'vs/base/common/themables'; import { Event, MicrotaskEmitter } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; import { ICommandAction, ICommandActionTitle, Icon, ILocalizedString } from 'vs/platform/action/common/action'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; @@ -18,6 +18,9 @@ import { IKeybindingRule, KeybindingsRegistry } from 'vs/platform/keybinding/com export interface IMenuItem { command: ICommandAction; alt?: ICommandAction; + /** + * Menu item is hidden if this expression returns false. + */ when?: ContextKeyExpression; group?: 'navigation' | string; order?: number; @@ -57,6 +60,7 @@ export class MenuId { static readonly DebugWatchContext = new MenuId('DebugWatchContext'); static readonly DebugToolBar = new MenuId('DebugToolBar'); static readonly DebugToolBarStop = new MenuId('DebugToolBarStop'); + static readonly DebugCallStackToolbar = new MenuId('DebugCallStackToolbar'); static readonly EditorContext = new MenuId('EditorContext'); static readonly SimpleEditorContext = new MenuId('SimpleEditorContext'); static readonly EditorContent = new MenuId('EditorContent'); @@ -109,6 +113,7 @@ export class MenuId { static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext'); static readonly SCMInputBox = new MenuId('SCMInputBox'); static readonly SCMChangesSeparator = new MenuId('SCMChangesSeparator'); + static readonly SCMChangesContext = new MenuId('SCMChangesContext'); static readonly SCMIncomingChanges = new MenuId('SCMIncomingChanges'); static readonly SCMIncomingChangesContext = new MenuId('SCMIncomingChangesContext'); static readonly SCMIncomingChangesSetting = new MenuId('SCMIncomingChangesSetting'); @@ -135,10 +140,12 @@ export class MenuId { static readonly StickyScrollContext = new MenuId('StickyScrollContext'); static readonly TestItem = new MenuId('TestItem'); static readonly TestItemGutter = new MenuId('TestItemGutter'); + static readonly TestProfilesContext = new MenuId('TestProfilesContext'); static readonly TestMessageContext = new MenuId('TestMessageContext'); static readonly TestMessageContent = new MenuId('TestMessageContent'); static readonly TestPeekElement = new MenuId('TestPeekElement'); static readonly TestPeekTitle = new MenuId('TestPeekTitle'); + static readonly TestCallStack = new MenuId('TestCallStack'); static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TitleBarTitleContext = new MenuId('TitleBarTitleContext'); @@ -168,6 +175,8 @@ export class MenuId { static readonly InteractiveCellDelete = new MenuId('InteractiveCellDelete'); static readonly InteractiveCellExecute = new MenuId('InteractiveCellExecute'); static readonly InteractiveInputExecute = new MenuId('InteractiveInputExecute'); + static readonly InteractiveInputConfig = new MenuId('InteractiveInputConfig'); + static readonly ReplInputExecute = new MenuId('ReplInputExecute'); static readonly IssueReporter = new MenuId('IssueReporter'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); static readonly NotebookStickyScrollContext = new MenuId('NotebookStickyScrollContext'); @@ -206,6 +215,7 @@ export class MenuId { static readonly TerminalStickyScrollContext = new MenuId('TerminalStickyScrollContext'); static readonly WebviewContext = new MenuId('WebviewContext'); static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); + static readonly InlineEditsActions = new MenuId('InlineEditsActions'); static readonly InlineEditActions = new MenuId('InlineEditActions'); static readonly NewFile = new MenuId('NewFile'); static readonly MergeInput1Toolbar = new MenuId('MergeToolbar1Toolbar'); @@ -216,6 +226,7 @@ export class MenuId { static readonly InlineEditToolbar = new MenuId('InlineEditToolbar'); static readonly ChatContext = new MenuId('ChatContext'); static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); + static readonly ChatCompareBlock = new MenuId('ChatCompareBlock'); static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteSecondary = new MenuId('ChatExecuteSecondary'); @@ -267,6 +278,11 @@ export interface IMenu extends IDisposable { getActions(options?: IMenuActionOptions): [string, Array][]; } +export interface IMenuData { + contexts: ReadonlySet; + actions: [string, Array][]; +} + export const IMenuService = createDecorator('menuService'); export interface IMenuCreateOptions { @@ -279,6 +295,8 @@ export interface IMenuService { readonly _serviceBrand: undefined; /** + * Consider using getMenuActions if you don't need to listen to events. + * * Create a new menu for the given menu identifier. A menu sends events when it's entries * have changed (placement, enablement, checked-state). By default it does not send events for * submenu entries. That is more expensive and must be explicitly enabled with the @@ -286,6 +304,16 @@ export interface IMenuService { */ createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu; + /** + * Creates a new menu, gets the actions, and then disposes of the menu. + */ + getMenuActions(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): [string, Array][]; + + /** + * Gets the names of the contexts that this menu listens on. + */ + getMenuContexts(id: MenuId): ReadonlySet; + /** * Reset **all** menu item hidden states. */ @@ -479,6 +507,7 @@ export class MenuItemAction implements IAction { alt: ICommandAction | undefined, options: IMenuActionOptions | undefined, readonly hideActions: IMenuItemHide | undefined, + readonly menuKeybinding: IAction | undefined, @IContextKeyService contextKeyService: IContextKeyService, @ICommandService private _commandService: ICommandService ) { @@ -513,7 +542,7 @@ export class MenuItemAction implements IAction { } this.item = item; - this.alt = alt ? new MenuItemAction(alt, undefined, options, hideActions, contextKeyService, _commandService) : undefined; + this.alt = alt ? new MenuItemAction(alt, undefined, options, hideActions, undefined, contextKeyService, _commandService) : undefined; this._options = options; this.class = icon && ThemeIcon.asClassName(icon); @@ -596,7 +625,7 @@ export abstract class Action2 { } export function registerAction2(ctor: { new(): Action2 }): IDisposable { - const disposables = new DisposableStore(); + const disposables: IDisposable[] = []; // not using `DisposableStore` to reduce startup perf cost const action = new ctor(); const { f1, menu, keybinding, ...command } = action.desc; @@ -606,7 +635,7 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { } // command - disposables.add(CommandsRegistry.registerCommand({ + disposables.push(CommandsRegistry.registerCommand({ id: command.id, handler: (accessor, ...args) => action.run(accessor, ...args), metadata: command.metadata, @@ -615,34 +644,38 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { // menu if (Array.isArray(menu)) { for (const item of menu) { - disposables.add(MenuRegistry.appendMenuItem(item.id, { command: { ...command, precondition: item.precondition === null ? undefined : command.precondition }, ...item })); + disposables.push(MenuRegistry.appendMenuItem(item.id, { command: { ...command, precondition: item.precondition === null ? undefined : command.precondition }, ...item })); } } else if (menu) { - disposables.add(MenuRegistry.appendMenuItem(menu.id, { command: { ...command, precondition: menu.precondition === null ? undefined : command.precondition }, ...menu })); + disposables.push(MenuRegistry.appendMenuItem(menu.id, { command: { ...command, precondition: menu.precondition === null ? undefined : command.precondition }, ...menu })); } if (f1) { - disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: command.precondition })); - disposables.add(MenuRegistry.addCommand(command)); + disposables.push(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: command.precondition })); + disposables.push(MenuRegistry.addCommand(command)); } // keybinding if (Array.isArray(keybinding)) { for (const item of keybinding) { - disposables.add(KeybindingsRegistry.registerKeybindingRule({ + disposables.push(KeybindingsRegistry.registerKeybindingRule({ ...item, id: command.id, when: command.precondition ? ContextKeyExpr.and(command.precondition, item.when) : item.when })); } } else if (keybinding) { - disposables.add(KeybindingsRegistry.registerKeybindingRule({ + disposables.push(KeybindingsRegistry.registerKeybindingRule({ ...keybinding, id: command.id, when: command.precondition ? ContextKeyExpr.and(command.precondition, keybinding.when) : keybinding.when })); } - return disposables; + return { + dispose() { + dispose(disposables); + } + }; } //#endregion diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 64a42990a54..91d9ffdc49d 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -10,10 +10,11 @@ import { IMenu, IMenuActionOptions, IMenuChangeEvent, IMenuCreateOptions, IMenuI import { ICommandAction, ILocalizedString } from 'vs/platform/action/common/action'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { Separator, toAction } from 'vs/base/common/actions'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { removeFastWithoutKeepingOrder } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class MenuService implements IMenuService { @@ -23,13 +24,26 @@ export class MenuService implements IMenuService { constructor( @ICommandService private readonly _commandService: ICommandService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, ) { this._hiddenStates = new PersistedMenuHideState(storageService); } createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu { - return new MenuImpl(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, contextKeyService); + return new MenuImpl(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, this._keybindingService, contextKeyService); + } + + getMenuActions(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): [string, Array][] { + const menu = new MenuImpl(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, this._keybindingService, contextKeyService); + const actions = menu.getActions(options); + menu.dispose(); + return actions; + } + + getMenuContexts(id: MenuId): ReadonlySet { + const menuInfo = new MenuInfoSnapshot(id, false); + return new Set([...menuInfo.structureContextKeys, ...menuInfo.preconditionContextKeys, ...menuInfo.toggledContextKeys]); } resetHiddenStates(ids?: MenuId[]): void { @@ -150,19 +164,15 @@ class PersistedMenuHideState { type MenuItemGroup = [string, Array]; -class MenuInfo { - - private _menuGroups: MenuItemGroup[] = []; +class MenuInfoSnapshot { + protected _menuGroups: MenuItemGroup[] = []; private _structureContextKeys: Set = new Set(); private _preconditionContextKeys: Set = new Set(); private _toggledContextKeys: Set = new Set(); constructor( - private readonly _id: MenuId, - private readonly _hiddenStates: PersistedMenuHideState, - private readonly _collectContextKeysForSubmenus: boolean, - @ICommandService private readonly _commandService: ICommandService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, + protected readonly _id: MenuId, + protected readonly _collectContextKeysForSubmenus: boolean, ) { this.refresh(); } @@ -187,10 +197,8 @@ class MenuInfo { this._preconditionContextKeys.clear(); this._toggledContextKeys.clear(); - const menuItems = MenuRegistry.getMenuItems(this._id); - + const menuItems = this._sort(MenuRegistry.getMenuItems(this._id)); let group: MenuItemGroup | undefined; - menuItems.sort(MenuInfo._compareMenuItems); for (const item of menuItems) { // group by groupId @@ -206,19 +214,24 @@ class MenuInfo { } } + protected _sort(menuItems: (IMenuItem | ISubmenuItem)[]) { + // no sorting needed in snapshot + return menuItems; + } + private _collectContextKeys(item: IMenuItem | ISubmenuItem): void { - MenuInfo._fillInKbExprKeys(item.when, this._structureContextKeys); + MenuInfoSnapshot._fillInKbExprKeys(item.when, this._structureContextKeys); if (isIMenuItem(item)) { // keep precondition keys for event if applicable if (item.command.precondition) { - MenuInfo._fillInKbExprKeys(item.command.precondition, this._preconditionContextKeys); + MenuInfoSnapshot._fillInKbExprKeys(item.command.precondition, this._preconditionContextKeys); } // keep toggled keys for event if applicable if (item.command.toggled) { const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; - MenuInfo._fillInKbExprKeys(toggledExpression, this._toggledContextKeys); + MenuInfoSnapshot._fillInKbExprKeys(toggledExpression, this._toggledContextKeys); } } else if (this._collectContextKeysForSubmenus) { @@ -228,13 +241,37 @@ class MenuInfo { } } + private static _fillInKbExprKeys(exp: ContextKeyExpression | undefined, set: Set): void { + if (exp) { + for (const key of exp.keys()) { + set.add(key); + } + } + } + +} + +class MenuInfo extends MenuInfoSnapshot { + + constructor( + _id: MenuId, + private readonly _hiddenStates: PersistedMenuHideState, + _collectContextKeysForSubmenus: boolean, + @ICommandService private readonly _commandService: ICommandService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(_id, _collectContextKeysForSubmenus); + this.refresh(); + } + createActionGroups(options: IMenuActionOptions | undefined): [string, Array][] { const result: [string, Array][] = []; for (const group of this._menuGroups) { const [id, items] = group; - const activeActions: Array = []; + let activeActions: Array | undefined; for (const item of items) { if (this._contextKeyService.contextMatchesRules(item.when)) { const isMenuItem = isIMenuItem(item); @@ -245,31 +282,27 @@ class MenuInfo { const menuHide = createMenuHide(this._id, isMenuItem ? item.command : item, this._hiddenStates); if (isMenuItem) { // MenuItemAction - activeActions.push(new MenuItemAction(item.command, item.alt, options, menuHide, this._contextKeyService, this._commandService)); - + const menuKeybinding = createConfigureKeybindingAction(this._commandService, this._keybindingService, item.command.id, item.when); + (activeActions ??= []).push(new MenuItemAction(item.command, item.alt, options, menuHide, menuKeybinding, this._contextKeyService, this._commandService)); } else { // SubmenuItemAction - const groups = new MenuInfo(item.submenu, this._hiddenStates, this._collectContextKeysForSubmenus, this._commandService, this._contextKeyService).createActionGroups(options); + const groups = new MenuInfo(item.submenu, this._hiddenStates, this._collectContextKeysForSubmenus, this._commandService, this._keybindingService, this._contextKeyService).createActionGroups(options); const submenuActions = Separator.join(...groups.map(g => g[1])); if (submenuActions.length > 0) { - activeActions.push(new SubmenuItemAction(item, menuHide, submenuActions)); + (activeActions ??= []).push(new SubmenuItemAction(item, menuHide, submenuActions)); } } } } - if (activeActions.length > 0) { + if (activeActions && activeActions.length > 0) { result.push([id, activeActions]); } } return result; } - private static _fillInKbExprKeys(exp: ContextKeyExpression | undefined, set: Set): void { - if (exp) { - for (const key of exp.keys()) { - set.add(key); - } - } + protected override _sort(menuItems: (IMenuItem | ISubmenuItem)[]): (IMenuItem | ISubmenuItem)[] { + return menuItems.sort(MenuInfo._compareMenuItems); } private static _compareMenuItems(a: IMenuItem | ISubmenuItem, b: IMenuItem | ISubmenuItem): number { @@ -336,9 +369,10 @@ class MenuImpl implements IMenu { hiddenStates: PersistedMenuHideState, options: Required, @ICommandService commandService: ICommandService, - @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService ) { - this._menuInfo = new MenuInfo(id, hiddenStates, options.emitEventsForSubmenuChanges, commandService, contextKeyService); + this._menuInfo = new MenuInfo(id, hiddenStates, options.emitEventsForSubmenuChanges, commandService, keybindingService, contextKeyService); // Rebuild this menu whenever the menu registry reports an event for this MenuId. // This usually happen while code and extensions are loaded and affects the over @@ -437,3 +471,18 @@ function createMenuHide(menu: MenuId, command: ICommandAction | ISubmenuItem, st get isHidden() { return !toggle.checked; }, }; } + +export function createConfigureKeybindingAction(commandService: ICommandService, keybindingService: IKeybindingService, commandId: string, when: ContextKeyExpression | undefined = undefined, enabled = true): IAction { + return toAction({ + id: `configureKeybinding/${commandId}`, + label: localize('configure keybinding', "Configure Keybinding"), + enabled, + run() { + // Only set the when clause when there is no keybinding + // It is possible that the action and the keybinding have different when clauses + const hasKeybinding = !!keybindingService.lookupKeybinding(commandId); // This may only be called inside the `run()` method as it can be expensive on startup. #210529 + const whenValue = !hasKeybinding && when ? when.serialize() : undefined; + commandService.executeCommand('workbench.action.openGlobalKeybindings', `@command:${commandId}` + (whenValue ? ` +when:${whenValue}` : '')); + } + }); +} diff --git a/src/vs/platform/actions/test/common/menuService.test.ts b/src/vs/platform/actions/test/common/menuService.test.ts index 70b864c76bd..8a3b2c421a9 100644 --- a/src/vs/platform/actions/test/common/menuService.test.ts +++ b/src/vs/platform/actions/test/common/menuService.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { isIMenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { MenuService } from 'vs/platform/actions/common/menuService'; import { NullCommandService } from 'vs/platform/commands/test/common/nullCommandService'; -import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; // --- service instances @@ -30,7 +30,7 @@ suite('MenuService', function () { let testMenuId: MenuId; setup(function () { - menuService = new MenuService(NullCommandService, new InMemoryStorageService()); + menuService = new MenuService(NullCommandService, new MockKeybindingService(), new InMemoryStorageService()); testMenuId = new MenuId(`testo/${generateUuid()}`); disposables.clear(); }); diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts index beb5028ebf4..5d726261ea4 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, WebContents } from 'electron'; +import { BrowserWindow, BrowserWindowConstructorOptions, WebContents } from 'electron'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; -import { hasNativeTitlebar } from 'vs/platform/window/common/window'; -import { IBaseWindow } from 'vs/platform/window/electron-main/window'; +import { hasNativeTitlebar, TitlebarStyle } from 'vs/platform/window/common/window'; +import { IBaseWindow, WindowMode } from 'vs/platform/window/electron-main/window'; import { BaseWindow } from 'vs/platform/windows/electron-main/windowImpl'; export interface IAuxiliaryWindow extends IBaseWindow { @@ -20,7 +20,7 @@ export interface IAuxiliaryWindow extends IBaseWindow { export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { - readonly id = this.contents.id; + readonly id = this.webContents.id; parentId = -1; override get win() { @@ -31,8 +31,10 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { return super.win; } + private stateApplied = false; + constructor( - private readonly contents: WebContents, + private readonly webContents: WebContents, @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @ILogService logService: ILogService, @IConfigurationService configurationService: IConfigurationService, @@ -45,25 +47,46 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { this.tryClaimWindow(); } - tryClaimWindow(): void { + tryClaimWindow(options?: BrowserWindowConstructorOptions): void { + if (this._store.isDisposed || this.webContents.isDestroyed()) { + return; // already disposed + } + + this.doTryClaimWindow(options); + + if (options && !this.stateApplied) { + this.stateApplied = true; + + this.applyState({ + x: options.x, + y: options.y, + width: options.width, + height: options.height, + // TODO@bpasero We currently do not support restoring fullscreen state for + // auxiliary windows because we do not get hold of the original `features` + // string that contains that info in `window-fullscreen`. However, we can + // probe the `options.show` value for whether the window should be maximized + // or not because we never show maximized windows initially to reduce flicker. + mode: options.show === false ? WindowMode.Maximized : WindowMode.Normal + }); + } + } + + private doTryClaimWindow(options?: BrowserWindowConstructorOptions): void { if (this._win) { return; // already claimed } - if (this._store.isDisposed || this.contents.isDestroyed()) { - return; // already disposed - } - - const window = BrowserWindow.fromWebContents(this.contents); + const window = BrowserWindow.fromWebContents(this.webContents); if (window) { this.logService.trace('[aux window] Claimed browser window instance'); // Remember - this.setWin(window); + this.setWin(window, options); // Disable Menu window.setMenu(null); - if ((isWindows || isLinux) && hasNativeTitlebar(this.configurationService)) { + if ((isWindows || isLinux) && hasNativeTitlebar(this.configurationService, options?.titleBarStyle === 'hidden' ? TitlebarStyle.CUSTOM : undefined /* unknown */)) { window.setAutoHideMenuBar(true); // Fix for https://github.com/microsoft/vscode/issues/200615 } @@ -71,4 +94,8 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { this.lifecycleMainService.registerAuxWindow(this); } } + + matches(webContents: WebContents): boolean { + return this.webContents.id === webContents.id; + } } diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts index 4ce7e22bbff..f79ec6bcbce 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts @@ -22,7 +22,7 @@ export interface IAuxiliaryWindowsMainService { createWindow(details: HandlerDetails): BrowserWindowConstructorOptions; registerWindow(webContents: WebContents): void; - getWindowById(windowId: number): IAuxiliaryWindow | undefined; + getWindowByWebContents(webContents: WebContents): IAuxiliaryWindow | undefined; getFocusedWindow(): IAuxiliaryWindow | undefined; getLastActiveWindow(): IAuxiliaryWindow | undefined; diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts index 1174386d1c2..a5829812e7a 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts @@ -12,8 +12,8 @@ import { AuxiliaryWindow, IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/e import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowState, defaultAuxWindowState } from 'vs/platform/window/electron-main/window'; -import { WindowStateValidator, defaultBrowserWindowOptions, getLastFocused } from 'vs/platform/windows/electron-main/windows'; +import { IWindowState, WindowMode, defaultAuxWindowState } from 'vs/platform/window/electron-main/window'; +import { IDefaultBrowserWindowOptionsOverrides, WindowStateValidator, defaultBrowserWindowOptions, getLastFocused } from 'vs/platform/windows/electron-main/windows'; export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliaryWindowsMainService { @@ -31,7 +31,7 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar private readonly _onDidTriggerSystemContextMenu = this._register(new Emitter<{ window: IAuxiliaryWindow; x: number; y: number }>()); readonly onDidTriggerSystemContextMenu = this._onDidTriggerSystemContextMenu.event; - private readonly windows = new Map(); + private readonly windows = new Map(); constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -51,16 +51,32 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar // is created. app.on('browser-window-created', (_event, browserWindow) => { - const auxiliaryWindow = this.getWindowById(browserWindow.id); + + // This is an auxiliary window, try to claim it + const auxiliaryWindow = this.getWindowByWebContents(browserWindow.webContents); if (auxiliaryWindow) { this.logService.trace('[aux window] app.on("browser-window-created"): Trying to claim auxiliary window'); auxiliaryWindow.tryClaimWindow(); } + + // This is a main window, listen to child windows getting created to claim it + else { + const disposables = new DisposableStore(); + disposables.add(Event.fromNodeEventEmitter(browserWindow.webContents, 'did-create-window', (browserWindow, details) => ({ browserWindow, details }))(({ browserWindow, details }) => { + const auxiliaryWindow = this.getWindowByWebContents(browserWindow.webContents); + if (auxiliaryWindow) { + this.logService.trace('[aux window] window.on("did-create-window"): Trying to claim auxiliary window'); + + auxiliaryWindow.tryClaimWindow(details.options); + } + })); + disposables.add(Event.fromNodeEventEmitter(browserWindow, 'closed')(() => disposables.dispose())); + } }); validatedIpcMain.handle('vscode:registerAuxiliaryWindow', async (event, mainWindowId: number) => { - const auxiliaryWindow = this.getWindowById(event.sender.id); + const auxiliaryWindow = this.getWindowByWebContents(event.sender); if (auxiliaryWindow) { this.logService.trace('[aux window] vscode:registerAuxiliaryWindow: Registering auxiliary window to main window'); @@ -72,15 +88,15 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar } createWindow(details: HandlerDetails): BrowserWindowConstructorOptions { - return this.instantiationService.invokeFunction(defaultBrowserWindowOptions, this.validateWindowState(details), { - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload-aux.js').fsPath - } + const { state, overrides } = this.computeWindowStateAndOverrides(details); + return this.instantiationService.invokeFunction(defaultBrowserWindowOptions, state, overrides, { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload-aux.js').fsPath }); } - private validateWindowState(details: HandlerDetails): IWindowState { + private computeWindowStateAndOverrides(details: HandlerDetails): { readonly state: IWindowState; readonly overrides: IDefaultBrowserWindowOptionsOverrides } { const windowState: IWindowState = {}; + const overrides: IDefaultBrowserWindowOptionsOverrides = {}; const features = details.features.split(','); // for example: popup=yes,left=270,top=14.5,width=800,height=600 for (const feature of features) { @@ -98,6 +114,18 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar case 'top': windowState.y = parseInt(value, 10); break; + case 'window-maximized': + windowState.mode = WindowMode.Maximized; + break; + case 'window-fullscreen': + windowState.mode = WindowMode.Fullscreen; + break; + case 'window-disable-fullscreen': + overrides.disableFullscreen = true; + break; + case 'window-native-titlebar': + overrides.forceNativeTitlebar = true; + break; } } @@ -105,7 +133,7 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar this.logService.trace('[aux window] using window state', state); - return state; + return { state, overrides }; } registerWindow(webContents: WebContents): void { @@ -125,14 +153,16 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar Event.once(auxiliaryWindow.onDidClose)(() => disposables.dispose()); } - getWindowById(windowId: number): AuxiliaryWindow | undefined { - return this.windows.get(windowId); + getWindowByWebContents(webContents: WebContents): AuxiliaryWindow | undefined { + const window = this.windows.get(webContents.id); + + return window?.matches(webContents) ? window : undefined; } getFocusedWindow(): IAuxiliaryWindow | undefined { const window = BrowserWindow.getFocusedWindow(); if (window) { - return this.getWindowById(window.id); + return this.getWindowByWebContents(window.webContents); } return undefined; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 34a5162b2d1..5ff0da22ae5 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createHash } from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; @@ -37,7 +37,7 @@ flakySuite('BackupMainService', () => { function toWorkspace(path: string): IWorkspaceIdentifier { return { - id: createHash('md5').update(sanitizePath(path)).digest('hex'), + id: createHash('md5').update(sanitizePath(path)).digest('hex'), // CodeQL [SM04514] Using MD5 to convert a file path to a fixed length configPath: URI.file(path) }; } @@ -45,7 +45,7 @@ flakySuite('BackupMainService', () => { function toWorkspaceBackupInfo(path: string, remoteAuthority?: string): IWorkspaceBackupInfo { return { workspace: { - id: createHash('md5').update(sanitizePath(path)).digest('hex'), + id: createHash('md5').update(sanitizePath(path)).digest('hex'), // CodeQL [SM04514] Using MD5 to convert a file path to a fixed length configPath: URI.file(path) }, remoteAuthority @@ -131,7 +131,7 @@ flakySuite('BackupMainService', () => { environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product }); - await Promises.mkdir(backupHome, { recursive: true }); + await fs.promises.mkdir(backupHome, { recursive: true }); configService = new TestConfigurationService(); stateMainService = new InMemoryTestStateMainService(); @@ -584,8 +584,8 @@ flakySuite('BackupMainService', () => { assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { - await Promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); - await Promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); + await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); + await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); } catch (error) { // ignore - folder might exist already } diff --git a/src/vs/platform/checksum/test/node/checksumService.test.ts b/src/vs/platform/checksum/test/node/checksumService.test.ts index 3e56af64720..5e7e71cdd7a 100644 --- a/src/vs/platform/checksum/test/node/checksumService.test.ts +++ b/src/vs/platform/checksum/test/node/checksumService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index f9a61f6ce3c..0f4a19424a0 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, isWebkitWebView } from 'vs/base/browser/browser'; -import { $, addDisposableListener, getActiveDocument, onDidRegisterWindow } from 'vs/base/browser/dom'; +import { $, addDisposableListener, getActiveDocument, getActiveWindow, isHTMLElement, onDidRegisterWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; import { DeferredPromise } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; @@ -15,6 +15,13 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ILogService } from 'vs/platform/log/common/log'; +/** + * Custom mime type used for storing a list of uris in the clipboard. + * + * Requires support for custom web clipboards https://github.com/w3c/clipboard-apis/pull/175 + */ +const vscodeResourcesMime = 'application/vnd.code.resources'; + export class BrowserClipboardService extends Disposable implements IClipboardService { declare readonly _serviceBrand: undefined; @@ -34,7 +41,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer // and not in the clipboard, we have to invalidate // that state when the user copies other data. this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => { - disposables.add(addDisposableListener(window.document, 'copy', () => this.clearResources())); + disposables.add(addDisposableListener(window.document, 'copy', () => this.clearResourcesState())); }, { window: mainWindow, disposables: this._store })); } @@ -66,7 +73,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer // This allows us to pass in a Promise that will either be cancelled by another event or // resolved with the contents of the first call to this.writeText. // see https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#parameters - navigator.clipboard.write([new ClipboardItem({ + getActiveWindow().navigator.clipboard.write([new ClipboardItem({ 'text/plain': currentWritePromise.p, })]).catch(async err => { if (!(err instanceof Error) || err.name !== 'NotAllowedError' || !currentWritePromise.isRejected) { @@ -86,7 +93,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer async writeText(text: string, type?: string): Promise { // Clear resources given we are writing text - this.writeResources([]); + this.clearResourcesState(); // With type: only in-memory is supported if (type) { @@ -106,7 +113,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer // as we have seen DOMExceptions in certain browsers // due to security policies. try { - return await navigator.clipboard.writeText(text); + return await getActiveWindow().navigator.clipboard.writeText(text); } catch (error) { console.error(error); } @@ -130,11 +137,11 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer activeDocument.execCommand('copy'); - if (activeElement instanceof HTMLElement) { + if (isHTMLElement(activeElement)) { activeElement.focus(); } - activeDocument.body.removeChild(textArea); + textArea.remove(); } async readText(type?: string): Promise { @@ -148,7 +155,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer // as we have seen DOMExceptions in certain browsers // due to security policies. try { - return await navigator.clipboard.readText(); + return await getActiveWindow().navigator.clipboard.readText(); } catch (error) { console.error(error); } @@ -172,8 +179,28 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer private static readonly MAX_RESOURCE_STATE_SOURCE_LENGTH = 1000; async writeResources(resources: URI[]): Promise { + // Guard access to navigator.clipboard with try/catch + // as we have seen DOMExceptions in certain browsers + // due to security policies. + try { + await getActiveWindow().navigator.clipboard.write([ + new ClipboardItem({ + [`web ${vscodeResourcesMime}`]: new Blob([ + JSON.stringify(resources.map(x => x.toJSON())) + ], { + type: vscodeResourcesMime + }) + }) + ]); + + // Continue to write to the in-memory clipboard as well. + // This is needed because some browsers allow the paste but then can't read the custom resources. + } catch (error) { + // Noop + } + if (resources.length === 0) { - this.clearResources(); + this.clearResourcesState(); } else { this.resources = resources; this.resourcesStateHash = await this.computeResourcesStateHash(); @@ -181,9 +208,25 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer } async readResources(): Promise { + // Guard access to navigator.clipboard with try/catch + // as we have seen DOMExceptions in certain browsers + // due to security policies. + try { + const items = await getActiveWindow().navigator.clipboard.read(); + for (const item of items) { + if (item.types.includes(`web ${vscodeResourcesMime}`)) { + const blob = await item.getType(`web ${vscodeResourcesMime}`); + const resources = (JSON.parse(await blob.text()) as URI[]).map(x => URI.from(x)); + return resources; + } + } + } catch (error) { + // Noop + } + const resourcesStateHash = await this.computeResourcesStateHash(); if (this.resourcesStateHash !== resourcesStateHash) { - this.clearResources(); // state mismatch, resources no longer valid + this.clearResourcesState(); // state mismatch, resources no longer valid } return this.resources; @@ -204,10 +247,28 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer } async hasResources(): Promise { + // Guard access to navigator.clipboard with try/catch + // as we have seen DOMExceptions in certain browsers + // due to security policies. + try { + const items = await getActiveWindow().navigator.clipboard.read(); + for (const item of items) { + if (item.types.includes(`web ${vscodeResourcesMime}`)) { + return true; + } + } + } catch (error) { + // Noop + } + return this.resources.length > 0; } - private clearResources(): void { + public clearInternalState(): void { + this.clearResourcesState(); + } + + private clearResourcesState(): void { this.resources = []; this.resourcesStateHash = undefined; } diff --git a/src/vs/platform/clipboard/common/clipboardService.ts b/src/vs/platform/clipboard/common/clipboardService.ts index c4aea9f7132..8cc5cbc397e 100644 --- a/src/vs/platform/clipboard/common/clipboardService.ts +++ b/src/vs/platform/clipboard/common/clipboardService.ts @@ -46,4 +46,11 @@ export interface IClipboardService { * Find out if resources are copied to the clipboard. */ hasResources(): Promise; + + /** + * Resets the internal state of the clipboard (if any) without touching the real clipboard. + * + * Used for implementations such as web which do not always support using the real clipboard. + */ + clearInternalState?(): void; } diff --git a/src/vs/platform/commands/test/common/commands.test.ts b/src/vs/platform/commands/test/common/commands.test.ts index eeb3ed5da2a..f46f8e1a6ae 100644 --- a/src/vs/platform/commands/test/common/commands.test.ts +++ b/src/vs/platform/commands/test/common/commands.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { combinedDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 559b3c18b55..55982dda176 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -229,6 +229,10 @@ export function addToValueTree(settingsTreeRoot: any, key: string, value: any, c obj = curr[s] = Object.create(null); break; case 'object': + if (obj === null) { + conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is null`); + return; + } break; default: conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`); diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index db1f4ee7006..553c312d6be 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -16,6 +16,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { addToValueTree, ConfigurationTarget, getConfigurationValue, IConfigurationChange, IConfigurationChangeEvent, IConfigurationCompareResult, IConfigurationData, IConfigurationModel, IConfigurationOverrides, IConfigurationUpdateOverrides, IConfigurationValue, IInspectValue, IOverrides, removeFromValueTree, toValuesTree } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions, IConfigurationPropertySchema, IConfigurationRegistry, overrideIdentifiersFromKey, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; import { FileOperation, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { Registry } from 'vs/platform/registry/common/platform'; import { Workspace } from 'vs/platform/workspace/common/workspace'; @@ -27,13 +28,18 @@ type InspectValue = IInspectValue & { merged?: V }; export class ConfigurationModel implements IConfigurationModel { + static createEmptyModel(logService: ILogService): ConfigurationModel { + return new ConfigurationModel({}, [], [], undefined, logService); + } + private readonly overrideConfigurations = new Map(); constructor( - private readonly _contents: any = {}, - private readonly _keys: string[] = [], - private readonly _overrides: IOverrides[] = [], - readonly raw?: ReadonlyArray | ConfigurationModel> + private readonly _contents: any, + private readonly _keys: string[], + private readonly _overrides: IOverrides[], + readonly raw: ReadonlyArray | ConfigurationModel> | undefined, + private readonly logService: ILogService ) { } @@ -45,7 +51,7 @@ export class ConfigurationModel implements IConfigurationModel { if (raw instanceof ConfigurationModel) { return raw; } - const parser = new ConfigurationModelParser(''); + const parser = new ConfigurationModelParser('', this.logService); parser.parseRaw(raw); return parser.configurationModel; }); @@ -93,7 +99,7 @@ export class ConfigurationModel implements IConfigurationModel { get overrides() { const overrides: { readonly identifiers: string[]; readonly value: V }[] = []; for (const { contents, identifiers, keys } of that.rawConfiguration.overrides) { - const value = new ConfigurationModel(contents, keys).getValue(section); + const value = new ConfigurationModel(contents, keys, [], undefined, that.logService).getValue(section); if (value !== undefined) { overrides.push({ identifiers, value }); } @@ -166,7 +172,7 @@ export class ConfigurationModel implements IConfigurationModel { } } } - return new ConfigurationModel(contents, keys, overrides, raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws); + return new ConfigurationModel(contents, keys, overrides, raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws, this.logService); } private createOverrideConfigurationModel(identifier: string): ConfigurationModel { @@ -197,7 +203,7 @@ export class ConfigurationModel implements IConfigurationModel { contents[key] = contentsForKey; } - return new ConfigurationModel(contents, this.keys, this.overrides); + return new ConfigurationModel(contents, this.keys, this.overrides, undefined, this.logService); } private mergeContents(source: any, target: any): void { @@ -267,17 +273,24 @@ export class ConfigurationModel implements IConfigurationModel { } private updateValue(key: string, value: any, add: boolean): void { - addToValueTree(this.contents, key, value, e => console.error(e)); + addToValueTree(this.contents, key, value, e => this.logService.error(e)); add = add || this.keys.indexOf(key) === -1; if (add) { this.keys.push(key); } if (OVERRIDE_PROPERTY_REGEX.test(key)) { - this.overrides.push({ - identifiers: overrideIdentifiersFromKey(key), + const identifiers = overrideIdentifiersFromKey(key); + const override = { + identifiers, keys: Object.keys(this.contents[key]), - contents: toValuesTree(this.contents[key], message => console.error(message)), - }); + contents: toValuesTree(this.contents[key], message => this.logService.error(message)), + }; + const index = this.overrides.findIndex(o => arrays.equals(o.identifiers, identifiers)); + if (index !== -1) { + this.overrides[index] = override; + } else { + this.overrides.push(override); + } } } } @@ -296,10 +309,13 @@ export class ConfigurationModelParser { private _restrictedConfigurations: string[] = []; private _parseErrors: any[] = []; - constructor(protected readonly _name: string) { } + constructor( + protected readonly _name: string, + protected readonly logService: ILogService + ) { } get configurationModel(): ConfigurationModel { - return this._configurationModel || new ConfigurationModel(); + return this._configurationModel || ConfigurationModel.createEmptyModel(this.logService); } get restrictedConfigurations(): string[] { @@ -326,7 +342,7 @@ export class ConfigurationModelParser { public parseRaw(raw: any, options?: ConfigurationParseOptions): void { this._raw = raw; const { contents, keys, overrides, restricted, hasExcludedProperties } = this.doParseRaw(raw, options); - this._configurationModel = new ConfigurationModel(contents, keys, overrides, hasExcludedProperties ? [raw] : undefined /* raw has not changed */); + this._configurationModel = new ConfigurationModel(contents, keys, overrides, hasExcludedProperties ? [raw] : undefined /* raw has not changed */, this.logService); this._restrictedConfigurations = restricted || []; } @@ -379,7 +395,7 @@ export class ConfigurationModelParser { json.visit(content, visitor); raw = currentParent[0] || {}; } catch (e) { - console.error(`Error while parsing settings file ${this._name}: ${e}`); + this.logService.error(`Error while parsing settings file ${this._name}: ${e}`); this._parseErrors = [e]; } } @@ -391,9 +407,9 @@ export class ConfigurationModelParser { const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); const filtered = this.filter(raw, configurationProperties, true, options); raw = filtered.raw; - const contents = toValuesTree(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); + const contents = toValuesTree(raw, message => this.logService.error(`Conflict in settings file ${this._name}: ${message}`)); const keys = Object.keys(raw); - const overrides = this.toOverrides(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); + const overrides = this.toOverrides(raw, message => this.logService.error(`Conflict in settings file ${this._name}: ${message}`)); return { contents, keys, overrides, restricted: filtered.restricted, hasExcludedProperties: filtered.hasExcludedProperties }; } @@ -459,10 +475,11 @@ export class UserSettings extends Disposable { private readonly userSettingsResource: URI, protected parseOptions: ConfigurationParseOptions, extUri: IExtUri, - private readonly fileService: IFileService + private readonly fileService: IFileService, + private readonly logService: ILogService, ) { super(); - this.parser = new ConfigurationModelParser(this.userSettingsResource.toString()); + this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), logService); this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource))); // Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134 this._register(this.fileService.watch(this.userSettingsResource)); @@ -478,7 +495,7 @@ export class UserSettings extends Disposable { this.parser.parse(content.value.toString() || '{}', this.parseOptions); return this.parser.configurationModel; } catch (e) { - return new ConfigurationModel(); + return ConfigurationModel.createEmptyModel(this.logService); } } @@ -678,11 +695,12 @@ export class Configuration { private _policyConfiguration: ConfigurationModel, private _applicationConfiguration: ConfigurationModel, private _localUserConfiguration: ConfigurationModel, - private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(), - private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), - private _folderConfigurations: ResourceMap = new ResourceMap(), - private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(), - private _memoryConfigurationByResource: ResourceMap = new ResourceMap() + private _remoteUserConfiguration: ConfigurationModel, + private _workspaceConfiguration: ConfigurationModel, + private _folderConfigurations: ResourceMap, + private _memoryConfiguration: ConfigurationModel, + private _memoryConfigurationByResource: ResourceMap, + private readonly logService: ILogService ) { } @@ -696,7 +714,7 @@ export class Configuration { if (overrides.resource) { memoryConfiguration = this._memoryConfigurationByResource.get(overrides.resource); if (!memoryConfiguration) { - memoryConfiguration = new ConfigurationModel(); + memoryConfiguration = ConfigurationModel.createEmptyModel(this.logService); this._memoryConfigurationByResource.set(overrides.resource, memoryConfiguration); } } else { @@ -1047,21 +1065,32 @@ export class Configuration { return [...keys.values()]; } - static parse(data: IConfigurationData): Configuration { - const defaultConfiguration = this.parseConfigurationModel(data.defaults); - const policyConfiguration = this.parseConfigurationModel(data.policy); - const applicationConfiguration = this.parseConfigurationModel(data.application); - const userConfiguration = this.parseConfigurationModel(data.user); - const workspaceConfiguration = this.parseConfigurationModel(data.workspace); + static parse(data: IConfigurationData, logService: ILogService): Configuration { + const defaultConfiguration = this.parseConfigurationModel(data.defaults, logService); + const policyConfiguration = this.parseConfigurationModel(data.policy, logService); + const applicationConfiguration = this.parseConfigurationModel(data.application, logService); + const userConfiguration = this.parseConfigurationModel(data.user, logService); + const workspaceConfiguration = this.parseConfigurationModel(data.workspace, logService); const folders: ResourceMap = data.folders.reduce((result, value) => { - result.set(URI.revive(value[0]), this.parseConfigurationModel(value[1])); + result.set(URI.revive(value[0]), this.parseConfigurationModel(value[1], logService)); return result; }, new ResourceMap()); - return new Configuration(defaultConfiguration, policyConfiguration, applicationConfiguration, userConfiguration, new ConfigurationModel(), workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap()); + return new Configuration( + defaultConfiguration, + policyConfiguration, + applicationConfiguration, + userConfiguration, + ConfigurationModel.createEmptyModel(logService), + workspaceConfiguration, + folders, + ConfigurationModel.createEmptyModel(logService), + new ResourceMap(), + logService + ); } - private static parseConfigurationModel(model: IConfigurationModel): ConfigurationModel { - return new ConfigurationModel(model.contents, model.keys, model.overrides); + private static parseConfigurationModel(model: IConfigurationModel, logService: ILogService): ConfigurationModel { + return new ConfigurationModel(model.contents, model.keys, model.overrides, undefined, logService); } } @@ -1097,7 +1126,13 @@ export class ConfigurationChangeEvent implements IConfigurationChangeEvent { readonly affectedKeys = new Set(); source!: ConfigurationTarget; - constructor(readonly change: IConfigurationChange, private readonly previous: { workspace?: Workspace; data: IConfigurationData } | undefined, private readonly currentConfiguraiton: Configuration, private readonly currentWorkspace?: Workspace) { + constructor( + readonly change: IConfigurationChange, + private readonly previous: { workspace?: Workspace; data: IConfigurationData } | undefined, + private readonly currentConfiguraiton: Configuration, + private readonly currentWorkspace: Workspace | undefined, + private readonly logService: ILogService + ) { for (const key of change.keys) { this.affectedKeys.add(key); } @@ -1117,7 +1152,7 @@ export class ConfigurationChangeEvent implements IConfigurationChangeEvent { private _previousConfiguration: Configuration | undefined = undefined; get previousConfiguration(): Configuration | undefined { if (!this._previousConfiguration && this.previous) { - this._previousConfiguration = Configuration.parse(this.previous.data); + this._previousConfiguration = Configuration.parse(this.previous.data, this.logService); } return this._previousConfiguration; } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index ed8e56d50c5..7876af5831e 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -70,10 +70,15 @@ export interface IConfigurationRegistry { */ deltaConfiguration(delta: IConfigurationDelta): void; + /** + * Return the registered default configurations + */ + getRegisteredDefaultConfigurations(): IConfigurationDefaults[]; + /** * Return the registered configuration defaults overrides */ - getConfigurationDefaultsOverrides(): Map; + getConfigurationDefaultsOverrides(): Map; /** * Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values. @@ -191,6 +196,11 @@ export interface IConfigurationPropertySchema extends IJSONSchema { */ disallowSyncIgnore?: boolean; + /** + * Disallow extensions to contribute configuration default value for this setting. + */ + disallowConfigurationDefault?: boolean; + /** * Labels for enumeration items */ @@ -233,22 +243,28 @@ export interface IConfigurationNode { restrictedProperties?: string[]; } +export type ConfigurationDefaultValueSource = IExtensionInfo | Map; + export interface IConfigurationDefaults { overrides: IStringDictionary; - source?: IExtensionInfo | string; + source?: IExtensionInfo; } export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & { defaultDefaultValue?: any; source?: IExtensionInfo; // Source of the Property - defaultValueSource?: IExtensionInfo | string; // Source of the Default Value + defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value }; -export type IConfigurationDefaultOverride = { +export interface IConfigurationDefaultOverride { readonly value: any; - readonly source?: IExtensionInfo | string; // Source of the default override - readonly valuesSources?: Map; // Source of each value in default language overrides -}; + readonly source?: IExtensionInfo; // Source of the default override +} + +export interface IConfigurationDefaultOverrideValue { + readonly value: any; + readonly source?: ConfigurationDefaultValueSource; +} export const allSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const applicationSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; @@ -264,7 +280,8 @@ const contributionRegistry = Registry.as(JSONExtensio class ConfigurationRegistry implements IConfigurationRegistry { - private readonly configurationDefaultsOverrides: Map; + private readonly registeredConfigurationDefaults: IConfigurationDefaults[] = []; + private readonly configurationDefaultsOverrides: Map; private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode; private readonly configurationContributors: IConfigurationNode[]; private readonly configurationProperties: IStringDictionary; @@ -280,7 +297,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { readonly onDidUpdateConfiguration = this._onDidUpdateConfiguration.event; constructor() { - this.configurationDefaultsOverrides = new Map(); + this.configurationDefaultsOverrides = new Map(); this.defaultLanguageConfigurationOverridesNode = { id: 'defaultOverrides', title: nls.localize('defaultLanguageConfigurationOverrides.title', "Default Language Configuration Overrides"), @@ -343,43 +360,47 @@ class ConfigurationRegistry implements IConfigurationRegistry { private doRegisterDefaultConfigurations(configurationDefaults: IConfigurationDefaults[], bucket: Set) { + this.registeredConfigurationDefaults.push(...configurationDefaults); + const overrideIdentifiers: string[] = []; for (const { overrides, source } of configurationDefaults) { for (const key in overrides) { bucket.add(key); + const configurationDefaultOverridesForKey = this.configurationDefaultsOverrides.get(key) + ?? this.configurationDefaultsOverrides.set(key, { configurationDefaultOverrides: [] }).get(key)!; + + const value = overrides[key]; + configurationDefaultOverridesForKey.configurationDefaultOverrides.push({ value, source }); + + // Configuration defaults for Override Identifiers if (OVERRIDE_PROPERTY_REGEX.test(key)) { - const configurationDefaultOverride = this.configurationDefaultsOverrides.get(key); - const valuesSources = configurationDefaultOverride?.valuesSources ?? new Map(); - if (source) { - for (const configuration of Object.keys(overrides[key])) { - valuesSources.set(configuration, source); - } + const newDefaultOverride = this.mergeDefaultConfigurationsForOverrideIdentifier(key, value, source, configurationDefaultOverridesForKey.configurationDefaultOverrideValue); + if (!newDefaultOverride) { + continue; } - const defaultValue = { ...(configurationDefaultOverride?.value || {}), ...overrides[key] }; - this.configurationDefaultsOverrides.set(key, { source, value: defaultValue, valuesSources }); - const plainKey = getLanguageTagSettingPlainKey(key); - const property: IRegisteredConfigurationPropertySchema = { - type: 'object', - default: defaultValue, - description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for the {0} language.", plainKey), - $ref: resourceLanguageSettingsSchemaId, - defaultDefaultValue: defaultValue, - source: types.isString(source) ? undefined : source, - defaultValueSource: source - }; + + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride; + this.updateDefaultOverrideProperty(key, newDefaultOverride, source); overrideIdentifiers.push(...overrideIdentifiersFromKey(key)); - this.configurationProperties[key] = property; - this.defaultLanguageConfigurationOverridesNode.properties![key] = property; - } else { - this.configurationDefaultsOverrides.set(key, { value: overrides[key], source }); + } + + // Configuration defaults for Configuration Properties + else { + const newDefaultOverride = this.mergeDefaultConfigurationsForConfigurationProperty(key, value, source, configurationDefaultOverridesForKey.configurationDefaultOverrideValue); + if (!newDefaultOverride) { + continue; + } + + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride; const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); } } + } } @@ -394,31 +415,147 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private doDeregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[], bucket: Set): void { + for (const defaultConfiguration of defaultConfigurations) { + const index = this.registeredConfigurationDefaults.indexOf(defaultConfiguration); + if (index !== -1) { + this.registeredConfigurationDefaults.splice(index, 1); + } + } for (const { overrides, source } of defaultConfigurations) { for (const key in overrides) { - const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key); - const id = types.isString(source) ? source : source?.id; - const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride?.source) ? configurationDefaultsOverride?.source : configurationDefaultsOverride?.source?.id; - if (id !== configurationDefaultsOverrideSourceId) { + const configurationDefaultOverridesForKey = this.configurationDefaultsOverrides.get(key); + if (!configurationDefaultOverridesForKey) { continue; } - bucket.add(key); - this.configurationDefaultsOverrides.delete(key); + + const index = configurationDefaultOverridesForKey.configurationDefaultOverrides + .findIndex(configurationDefaultOverride => source ? configurationDefaultOverride.source?.id === source.id : configurationDefaultOverride.value === overrides[key]); + if (index === -1) { + continue; + } + + configurationDefaultOverridesForKey.configurationDefaultOverrides.splice(index, 1); + if (configurationDefaultOverridesForKey.configurationDefaultOverrides.length === 0) { + this.configurationDefaultsOverrides.delete(key); + } + if (OVERRIDE_PROPERTY_REGEX.test(key)) { - delete this.configurationProperties[key]; - delete this.defaultLanguageConfigurationOverridesNode.properties![key]; + let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined; + for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { + configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForOverrideIdentifier(key, configurationDefaultOverride.value, configurationDefaultOverride.source, configurationDefaultOverrideValue); + } + if (configurationDefaultOverrideValue && !types.isEmptyObject(configurationDefaultOverrideValue.value)) { + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue; + this.updateDefaultOverrideProperty(key, configurationDefaultOverrideValue, source); + } else { + this.configurationDefaultsOverrides.delete(key); + delete this.configurationProperties[key]; + delete this.defaultLanguageConfigurationOverridesNode.properties![key]; + } } else { + let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined; + for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { + configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForConfigurationProperty(key, configurationDefaultOverride.value, configurationDefaultOverride.source, configurationDefaultOverrideValue); + } + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue; const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); } } + bucket.add(key); + } + } + this.updateOverridePropertyPatternKey(); + } + + private updateDefaultOverrideProperty(key: string, newDefaultOverride: IConfigurationDefaultOverrideValue, source: IExtensionInfo | undefined): void { + const property: IRegisteredConfigurationPropertySchema = { + type: 'object', + default: newDefaultOverride.value, + description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for the {0} language.", getLanguageTagSettingPlainKey(key)), + $ref: resourceLanguageSettingsSchemaId, + defaultDefaultValue: newDefaultOverride.value, + source, + defaultValueSource: source + }; + this.configurationProperties[key] = property; + this.defaultLanguageConfigurationOverridesNode.properties![key] = property; + } + + private mergeDefaultConfigurationsForOverrideIdentifier(overrideIdentifier: string, configurationValueObject: IStringDictionary, valueSource: IExtensionInfo | undefined, existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined): IConfigurationDefaultOverrideValue | undefined { + const defaultValue = existingDefaultOverride?.value || {}; + const source = existingDefaultOverride?.source ?? new Map(); + + // This should not happen + if (!(source instanceof Map)) { + console.error('objectConfigurationSources is not a Map'); + return undefined; + } + + for (const propertyKey of Object.keys(configurationValueObject)) { + const propertyDefaultValue = configurationValueObject[propertyKey]; + + const isObjectSetting = types.isObject(propertyDefaultValue) && + (types.isUndefined(defaultValue[propertyKey]) || types.isObject(defaultValue[propertyKey])); + + // If the default value is an object, merge the objects and store the source of each keys + if (isObjectSetting) { + defaultValue[propertyKey] = { ...(defaultValue[propertyKey] ?? {}), ...propertyDefaultValue }; + // Track the source of each value in the object + if (valueSource) { + for (const objectKey in propertyDefaultValue) { + source.set(`${propertyKey}.${objectKey}`, valueSource); + } + } + } + + // Primitive values are overridden + else { + defaultValue[propertyKey] = propertyDefaultValue; + if (valueSource) { + source.set(propertyKey, valueSource); + } else { + source.delete(propertyKey); + } } } - this.updateOverridePropertyPatternKey(); + return { value: defaultValue, source }; + } + + private mergeDefaultConfigurationsForConfigurationProperty(propertyKey: string, value: any, valuesSource: IExtensionInfo | undefined, existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined): IConfigurationDefaultOverrideValue | undefined { + const property = this.configurationProperties[propertyKey]; + const existingDefaultValue = existingDefaultOverride?.value ?? property?.defaultDefaultValue; + let source: ConfigurationDefaultValueSource | undefined = valuesSource; + + const isObjectSetting = types.isObject(value) && + ( + property !== undefined && property.type === 'object' || + property === undefined && (types.isUndefined(existingDefaultValue) || types.isObject(existingDefaultValue)) + ); + + // If the default value is an object, merge the objects and store the source of each keys + if (isObjectSetting) { + source = existingDefaultOverride?.source ?? new Map(); + + // This should not happen + if (!(source instanceof Map)) { + console.error('defaultValueSource is not a Map'); + return undefined; + } + + for (const objectKey in value) { + if (valuesSource) { + source.set(`${propertyKey}.${objectKey}`, valuesSource); + } + } + value = { ...(types.isObject(existingDefaultValue) ? existingDefaultValue : {}), ...value }; + } + + return { value, source }; } public deltaConfiguration(delta: IConfigurationDelta): void { @@ -569,8 +706,18 @@ class ConfigurationRegistry implements IConfigurationRegistry { return this.excludedConfigurationProperties; } - getConfigurationDefaultsOverrides(): Map { - return this.configurationDefaultsOverrides; + getRegisteredDefaultConfigurations(): IConfigurationDefaults[] { + return [...this.registeredConfigurationDefaults]; + } + + getConfigurationDefaultsOverrides(): Map { + const configurationDefaultsOverrides = new Map(); + for (const [key, value] of this.configurationDefaultsOverrides) { + if (value.configurationDefaultOverrideValue) { + configurationDefaultsOverrides.set(key, value.configurationDefaultOverrideValue); + } + } + return configurationDefaultsOverrides; } private registerJSONConfiguration(configuration: IConfigurationNode) { @@ -671,9 +818,15 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void { - const configurationdefaultOverride = this.configurationDefaultsOverrides.get(key); - let defaultValue = configurationdefaultOverride?.value; - let defaultSource = configurationdefaultOverride?.source; + const configurationdefaultOverride = this.configurationDefaultsOverrides.get(key)?.configurationDefaultOverrideValue; + let defaultValue = undefined; + let defaultSource = undefined; + if (configurationdefaultOverride + && (!property.disallowConfigurationDefault || !configurationdefaultOverride.source) // Prevent overriding the default value if the property is disallowed to be overridden by configuration defaults from extensions + ) { + defaultValue = configurationdefaultOverride.value; + defaultSource = configurationdefaultOverride.source; + } if (types.isUndefined(defaultValue)) { defaultValue = property.defaultDefaultValue; defaultSource = undefined; @@ -758,3 +911,36 @@ export function getScopes(): [string, ConfigurationScope | undefined][] { scopes.push(['task', ConfigurationScope.RESOURCE]); return scopes; } + +export function getAllConfigurationProperties(configurationNode: IConfigurationNode[]): IStringDictionary { + const result: IStringDictionary = {}; + for (const configuration of configurationNode) { + const properties = configuration.properties; + if (types.isObject(properties)) { + for (const key in properties) { + result[key] = properties[key]; + } + } + if (configuration.allOf) { + Object.assign(result, getAllConfigurationProperties(configuration.allOf)); + } + } + return result; +} + +export function parseScope(scope: string): ConfigurationScope { + switch (scope) { + case 'application': + return ConfigurationScope.APPLICATION; + case 'machine': + return ConfigurationScope.MACHINE; + case 'resource': + return ConfigurationScope.RESOURCE; + case 'machine-overridable': + return ConfigurationScope.MACHINE_OVERRIDABLE; + case 'language-overridable': + return ConfigurationScope.LANGUAGE_OVERRIDABLE; + default: + return ConfigurationScope.WINDOW; + } +} diff --git a/src/vs/platform/configuration/common/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts index 051d7f2e771..9de7cb2d650 100644 --- a/src/vs/platform/configuration/common/configurationService.ts +++ b/src/vs/platform/configuration/common/configurationService.ts @@ -11,6 +11,7 @@ import { JSONPath, ParseError, parse } from 'vs/base/common/json'; import { applyEdits, setProperty } from 'vs/base/common/jsonEdit'; import { Edit, FormattingOptions } from 'vs/base/common/jsonFormatter'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { equals } from 'vs/base/common/objects'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; @@ -42,13 +43,24 @@ export class ConfigurationService extends Disposable implements IConfigurationSe private readonly settingsResource: URI, fileService: IFileService, policyService: IPolicyService, - logService: ILogService, + private readonly logService: ILogService, ) { super(); - this.defaultConfiguration = this._register(new DefaultConfiguration()); + this.defaultConfiguration = this._register(new DefaultConfiguration(logService)); this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService)); - this.userConfiguration = this._register(new UserSettings(this.settingsResource, {}, extUriBiasedIgnorePathCase, fileService)); - this.configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel()); + this.userConfiguration = this._register(new UserSettings(this.settingsResource, {}, extUriBiasedIgnorePathCase, fileService, logService)); + this.configuration = new Configuration( + this.defaultConfiguration.configurationModel, + this.policyConfiguration.configurationModel, + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ResourceMap(), + ConfigurationModel.createEmptyModel(logService), + new ResourceMap(), + logService + ); this.configurationEditing = new ConfigurationEditing(settingsResource, fileService, this); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50)); @@ -59,7 +71,18 @@ export class ConfigurationService extends Disposable implements IConfigurationSe async initialize(): Promise { const [defaultModel, policyModel, userModel] = await Promise.all([this.defaultConfiguration.initialize(), this.policyConfiguration.initialize(), this.userConfiguration.loadConfiguration()]); - this.configuration = new Configuration(defaultModel, policyModel, new ConfigurationModel(), userModel); + this.configuration = new Configuration( + defaultModel, + policyModel, + ConfigurationModel.createEmptyModel(this.logService), + userModel, + ConfigurationModel.createEmptyModel(this.logService), + ConfigurationModel.createEmptyModel(this.logService), + new ResourceMap(), + ConfigurationModel.createEmptyModel(this.logService), + new ResourceMap(), + this.logService + ); } getConfigurationData(): IConfigurationData { @@ -157,7 +180,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe } private trigger(configurationChange: IConfigurationChange, previous: IConfigurationData, source: ConfigurationTarget): void { - const event = new ConfigurationChangeEvent(configurationChange, { data: previous }, this.configuration); + const event = new ConfigurationChangeEvent(configurationChange, { data: previous }, this.configuration, undefined, this.logService); event.source = source; this._onDidChangeConfiguration.fire(event); } diff --git a/src/vs/platform/configuration/common/configurations.ts b/src/vs/platform/configuration/common/configurations.ts index 46ba3c61c2d..5e3c303ada6 100644 --- a/src/vs/platform/configuration/common/configurations.ts +++ b/src/vs/platform/configuration/common/configurations.ts @@ -11,7 +11,7 @@ import { equals } from 'vs/base/common/objects'; import { isEmptyObject } from 'vs/base/common/types'; import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { Extensions, IConfigurationRegistry, IRegisteredConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; -import { ILogService } from 'vs/platform/log/common/log'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IPolicyService, PolicyDefinition, PolicyName, PolicyValue } from 'vs/platform/policy/common/policy'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -20,11 +20,15 @@ export class DefaultConfiguration extends Disposable { private readonly _onDidChangeConfiguration = this._register(new Emitter<{ defaults: ConfigurationModel; properties: string[] }>()); readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - private _configurationModel = new ConfigurationModel(); + private _configurationModel = ConfigurationModel.createEmptyModel(this.logService); get configurationModel(): ConfigurationModel { return this._configurationModel; } + constructor(private readonly logService: ILogService) { + super(); + } + async initialize(): Promise { this.resetConfigurationModel(); this._register(Registry.as(Extensions.Configuration).onDidUpdateConfiguration(({ properties, defaultsOverrides }) => this.onDidUpdateConfiguration(Array.from(properties), defaultsOverrides))); @@ -46,7 +50,7 @@ export class DefaultConfiguration extends Disposable { } private resetConfigurationModel(): void { - this._configurationModel = new ConfigurationModel(); + this._configurationModel = ConfigurationModel.createEmptyModel(this.logService); const properties = Registry.as(Extensions.Configuration).getConfigurationProperties(); this.updateConfigurationModel(Object.keys(properties), properties); } @@ -57,9 +61,9 @@ export class DefaultConfiguration extends Disposable { const defaultOverrideValue = configurationDefaultsOverrides[key]; const propertySchema = configurationProperties[key]; if (defaultOverrideValue !== undefined) { - this._configurationModel.addValue(key, defaultOverrideValue); + this._configurationModel.setValue(key, defaultOverrideValue); } else if (propertySchema) { - this._configurationModel.addValue(key, propertySchema.default); + this._configurationModel.setValue(key, propertySchema.default); } else { this._configurationModel.removeValue(key); } @@ -76,7 +80,7 @@ export interface IPolicyConfiguration { export class NullPolicyConfiguration implements IPolicyConfiguration { readonly onDidChangeConfiguration = Event.None; - readonly configurationModel = new ConfigurationModel(); + readonly configurationModel = ConfigurationModel.createEmptyModel(new NullLogService()); async initialize() { return this.configurationModel; } } @@ -85,7 +89,7 @@ export class PolicyConfiguration extends Disposable implements IPolicyConfigurat private readonly _onDidChangeConfiguration = this._register(new Emitter()); readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - private _configurationModel = new ConfigurationModel(); + private _configurationModel = ConfigurationModel.createEmptyModel(this.logService); get configurationModel() { return this._configurationModel; } constructor( @@ -164,7 +168,7 @@ export class PolicyConfiguration extends Disposable implements IPolicyConfigurat if (changed.length) { this.logService.trace('PolicyConfiguration#changed', changed); const old = this._configurationModel; - this._configurationModel = new ConfigurationModel(); + this._configurationModel = ConfigurationModel.createEmptyModel(this.logService); for (const key of old.keys) { this._configurationModel.setValue(key, old.getValue(key)); } diff --git a/src/vs/platform/configuration/test/common/configuration.test.ts b/src/vs/platform/configuration/test/common/configuration.test.ts index e7b169e7341..1f709cf2c0f 100644 --- a/src/vs/platform/configuration/test/common/configuration.test.ts +++ b/src/vs/platform/configuration/test/common/configuration.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { merge, removeFromValueTree } from 'vs/platform/configuration/common/configuration'; import { mergeChanges } from 'vs/platform/configuration/common/configurationModels'; diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 605ae8b6517..ab425c4fd30 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -2,12 +2,14 @@ * 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 assert from 'assert'; +import { ResourceMap } from 'vs/base/common/map'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Configuration, ConfigurationChangeEvent, ConfigurationModel, ConfigurationModelParser, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { NullLogService } from 'vs/platform/log/common/log'; import { Registry } from 'vs/platform/registry/common/platform'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; @@ -30,7 +32,7 @@ suite('ConfigurationModelParser', () => { }); test('parse configuration model with single override identifier', () => { - const testObject = new ConfigurationModelParser(''); + const testObject = new ConfigurationModelParser('', new NullLogService()); testObject.parse(JSON.stringify({ '[x]': { 'a': 1 } })); @@ -38,7 +40,7 @@ suite('ConfigurationModelParser', () => { }); test('parse configuration model with multiple override identifiers', () => { - const testObject = new ConfigurationModelParser(''); + const testObject = new ConfigurationModelParser('', new NullLogService()); testObject.parse(JSON.stringify({ '[x][y]': { 'a': 1 } })); @@ -46,7 +48,7 @@ suite('ConfigurationModelParser', () => { }); test('parse configuration model with multiple duplicate override identifiers', () => { - const testObject = new ConfigurationModelParser(''); + const testObject = new ConfigurationModelParser('', new NullLogService()); testObject.parse(JSON.stringify({ '[x][y][x][z]': { 'a': 1 } })); @@ -54,7 +56,7 @@ suite('ConfigurationModelParser', () => { }); test('parse configuration model with exclude option', () => { - const testObject = new ConfigurationModelParser(''); + const testObject = new ConfigurationModelParser('', new NullLogService()); testObject.parse(JSON.stringify({ 'a': 1, 'b': 2 }), { exclude: ['a'] }); @@ -63,7 +65,7 @@ suite('ConfigurationModelParser', () => { }); test('parse configuration model with exclude option even included', () => { - const testObject = new ConfigurationModelParser(''); + const testObject = new ConfigurationModelParser('', new NullLogService()); testObject.parse(JSON.stringify({ 'a': 1, 'b': 2 }), { exclude: ['a'], include: ['a'] }); @@ -72,7 +74,7 @@ suite('ConfigurationModelParser', () => { }); test('parse configuration model with scopes filter', () => { - const testObject = new ConfigurationModelParser(''); + const testObject = new ConfigurationModelParser('', new NullLogService()); testObject.parse(JSON.stringify({ 'ConfigurationModelParserTest.windowSetting': '1' }), { scopes: [ConfigurationScope.APPLICATION] }); @@ -80,13 +82,23 @@ suite('ConfigurationModelParser', () => { }); test('parse configuration model with include option', () => { - const testObject = new ConfigurationModelParser(''); + const testObject = new ConfigurationModelParser('', new NullLogService()); testObject.parse(JSON.stringify({ 'ConfigurationModelParserTest.windowSetting': '1' }), { include: ['ConfigurationModelParserTest.windowSetting'], scopes: [ConfigurationScope.APPLICATION] }); assert.strictEqual(testObject.configurationModel.getValue('ConfigurationModelParserTest.windowSetting'), '1'); }); + test('parse configuration model with invalid setting key', () => { + const testObject = new ConfigurationModelParser('', new NullLogService()); + + testObject.parse(JSON.stringify({ 'a': null, 'a.b.c': { c: 1 } })); + + assert.strictEqual(testObject.configurationModel.getValue('a'), null); + assert.strictEqual(testObject.configurationModel.getValue('a.b'), undefined); + assert.strictEqual(testObject.configurationModel.getValue('a.b.c'), undefined); + }); + }); suite('ConfigurationModel', () => { @@ -94,7 +106,7 @@ suite('ConfigurationModel', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('setValue for a key that has no sections and not defined', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b']); + const testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b'], [], undefined, new NullLogService()); testObject.setValue('f', 1); @@ -103,7 +115,7 @@ suite('ConfigurationModel', () => { }); test('setValue for a key that has no sections and defined', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f']); + const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f'], [], undefined, new NullLogService()); testObject.setValue('f', 3); @@ -112,7 +124,7 @@ suite('ConfigurationModel', () => { }); test('setValue for a key that has sections and not defined', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f']); + const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f'], [], undefined, new NullLogService()); testObject.setValue('b.c', 1); @@ -126,7 +138,7 @@ suite('ConfigurationModel', () => { }); test('setValue for a key that has sections and defined', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'b': { 'c': 1 }, 'f': 1 }, ['a.b', 'b.c', 'f']); + const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'b': { 'c': 1 }, 'f': 1 }, ['a.b', 'b.c', 'f'], [], undefined, new NullLogService()); testObject.setValue('b.c', 3); @@ -135,7 +147,7 @@ suite('ConfigurationModel', () => { }); test('setValue for a key that has sections and sub section not defined', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f']); + const testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f'], [], undefined, new NullLogService()); testObject.setValue('a.c', 1); @@ -144,7 +156,7 @@ suite('ConfigurationModel', () => { }); test('setValue for a key that has sections and sub section defined', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 1, 'c': 1 }, 'f': 1 }, ['a.b', 'a.c', 'f']); + const testObject = new ConfigurationModel({ 'a': { 'b': 1, 'c': 1 }, 'f': 1 }, ['a.b', 'a.c', 'f'], [], undefined, new NullLogService()); testObject.setValue('a.c', 3); @@ -153,7 +165,7 @@ suite('ConfigurationModel', () => { }); test('setValue for a key that has sections and last section is added', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': {} }, 'f': 1 }, ['a.b', 'f']); + const testObject = new ConfigurationModel({ 'a': { 'b': {} }, 'f': 1 }, ['a.b', 'f'], [], undefined, new NullLogService()); testObject.setValue('a.b.c', 1); @@ -162,7 +174,7 @@ suite('ConfigurationModel', () => { }); test('removeValue: remove a non existing key', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b']); + const testObject = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [], undefined, new NullLogService()); testObject.removeValue('a.b.c'); @@ -171,7 +183,7 @@ suite('ConfigurationModel', () => { }); test('removeValue: remove a single segmented key', () => { - const testObject = new ConfigurationModel({ 'a': 1 }, ['a']); + const testObject = new ConfigurationModel({ 'a': 1 }, ['a'], [], undefined, new NullLogService()); testObject.removeValue('a'); @@ -180,7 +192,7 @@ suite('ConfigurationModel', () => { }); test('removeValue: remove a multi segmented key', () => { - const testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b']); + const testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b'], [], undefined, new NullLogService()); testObject.removeValue('a.b'); @@ -191,7 +203,7 @@ suite('ConfigurationModel', () => { test('get overriding configuration model for an existing identifier', () => { const testObject = new ConfigurationModel( { 'a': { 'b': 1 }, 'f': 1 }, [], - [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]); + [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }], [], new NullLogService()); assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 }); }); @@ -199,7 +211,7 @@ suite('ConfigurationModel', () => { test('get overriding configuration model for an identifier that does not exist', () => { const testObject = new ConfigurationModel( { 'a': { 'b': 1 }, 'f': 1 }, [], - [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]); + [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }], [], new NullLogService()); assert.deepStrictEqual(testObject.override('xyz').contents, { 'a': { 'b': 1 }, 'f': 1 }); }); @@ -207,7 +219,7 @@ suite('ConfigurationModel', () => { test('get overriding configuration when one of the keys does not exist in base', () => { const testObject = new ConfigurationModel( { 'a': { 'b': 1 }, 'f': 1 }, [], - [{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'g': 1 }, keys: ['a', 'g'] }]); + [{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'g': 1 }, keys: ['a', 'g'] }], [], new NullLogService()); assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1, 'g': 1 }); }); @@ -215,7 +227,7 @@ suite('ConfigurationModel', () => { test('get overriding configuration when one of the key in base is not of object type', () => { const testObject = new ConfigurationModel( { 'a': { 'b': 1 }, 'f': 1 }, [], - [{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': { 'g': 1 } }, keys: ['a', 'f'] }]); + [{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': { 'g': 1 } }, keys: ['a', 'f'] }], [], new NullLogService()); assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': { 'g': 1 } }); }); @@ -223,7 +235,7 @@ suite('ConfigurationModel', () => { test('get overriding configuration when one of the key in overriding contents is not of object type', () => { const testObject = new ConfigurationModel( { 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [], - [{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': 1 }, keys: ['a', 'f'] }]); + [{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': 1 }, keys: ['a', 'f'] }], [], new NullLogService()); assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 }); }); @@ -231,7 +243,7 @@ suite('ConfigurationModel', () => { test('get overriding configuration if the value of overriding identifier is not object', () => { const testObject = new ConfigurationModel( { 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [], - [{ identifiers: ['c'], contents: 'abc', keys: [] }]); + [{ identifiers: ['c'], contents: 'abc', keys: [] }], [], new NullLogService()); assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } }); }); @@ -239,14 +251,14 @@ suite('ConfigurationModel', () => { test('get overriding configuration if the value of overriding identifier is an empty object', () => { const testObject = new ConfigurationModel( { 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [], - [{ identifiers: ['c'], contents: {}, keys: [] }]); + [{ identifiers: ['c'], contents: {}, keys: [] }], [], new NullLogService()); assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } }); }); test('simple merge', () => { - const base = new ConfigurationModel({ 'a': 1, 'b': 2 }, ['a', 'b']); - const add = new ConfigurationModel({ 'a': 3, 'c': 4 }, ['a', 'c']); + const base = new ConfigurationModel({ 'a': 1, 'b': 2 }, ['a', 'b'], [], undefined, new NullLogService()); + const add = new ConfigurationModel({ 'a': 3, 'c': 4 }, ['a', 'c'], [], undefined, new NullLogService()); const result = base.merge(add); assert.deepStrictEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 }); @@ -254,8 +266,8 @@ suite('ConfigurationModel', () => { }); test('recursive merge', () => { - const base = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b']); - const add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b']); + const base = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b'], [], undefined, new NullLogService()); + const add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [], undefined, new NullLogService()); const result = base.merge(add); assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 } }); @@ -264,8 +276,8 @@ suite('ConfigurationModel', () => { }); test('simple merge overrides', () => { - const base = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': 2 }, keys: ['a'] }]); - const add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'b': 2 }, keys: ['b'] }]); + const base = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': 2 }, keys: ['a'] }], undefined, new NullLogService()); + const add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'b': 2 }, keys: ['b'] }], undefined, new NullLogService()); const result = base.merge(add); assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 } }); @@ -275,8 +287,8 @@ suite('ConfigurationModel', () => { }); test('recursive merge overrides', () => { - const base = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f'], [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]); - const add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': { 'e': 2 } }, keys: ['a'] }]); + const base = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f'], [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }], undefined, new NullLogService()); + const add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': { 'e': 2 } }, keys: ['a'] }], undefined, new NullLogService()); const result = base.merge(add); assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 }, 'f': 1 }); @@ -286,27 +298,27 @@ suite('ConfigurationModel', () => { }); test('Test contents while getting an existing property', () => { - let testObject = new ConfigurationModel({ 'a': 1 }); + let testObject = new ConfigurationModel({ 'a': 1 }, [], [], undefined, new NullLogService()); assert.deepStrictEqual(testObject.getValue('a'), 1); - testObject = new ConfigurationModel({ 'a': { 'b': 1 } }); + testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, [], [], undefined, new NullLogService()); assert.deepStrictEqual(testObject.getValue('a'), { 'b': 1 }); }); test('Test contents are undefined for non existing properties', () => { - const testObject = new ConfigurationModel({ awesome: true }); + const testObject = new ConfigurationModel({ awesome: true }, [], [], undefined, new NullLogService()); assert.deepStrictEqual(testObject.getValue('unknownproperty'), undefined); }); test('Test override gives all content merged with overrides', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, [], [{ identifiers: ['b'], contents: { 'a': 2 }, keys: ['a'] }]); + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, [], [{ identifiers: ['b'], contents: { 'a': 2 }, keys: ['a'] }], undefined, new NullLogService()); assert.deepStrictEqual(testObject.override('b').contents, { 'a': 2, 'c': 1 }); }); test('Test override when an override has multiple identifiers', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }]); + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }], undefined, new NullLogService()); let actual = testObject.override('x'); assert.deepStrictEqual(actual.contents, { 'a': 2, 'c': 1 }); @@ -320,7 +332,7 @@ suite('ConfigurationModel', () => { }); test('Test override when an identifier is defined in multiple overrides', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x'], contents: { 'a': 3, 'b': 1 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }]); + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x'], contents: { 'a': 3, 'b': 1 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }], undefined, new NullLogService()); const actual = testObject.override('x'); assert.deepStrictEqual(actual.contents, { 'a': 3, 'c': 1, 'b': 1 }); @@ -330,8 +342,8 @@ suite('ConfigurationModel', () => { }); test('Test merge when configuration models have multiple identifiers', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['y'], contents: { 'c': 1 }, keys: ['c'] }, { identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }]); - const target = new ConfigurationModel({ 'a': 2, 'b': 1 }, ['a', 'b'], [{ identifiers: ['x'], contents: { 'a': 3, 'b': 2 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { 'b': 3 }, keys: ['b'] }]); + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['y'], contents: { 'c': 1 }, keys: ['c'] }, { identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }], undefined, new NullLogService()); + const target = new ConfigurationModel({ 'a': 2, 'b': 1 }, ['a', 'b'], [{ identifiers: ['x'], contents: { 'a': 3, 'b': 2 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { 'b': 3 }, keys: ['b'] }], undefined, new NullLogService()); const actual = testObject.merge(target); @@ -345,7 +357,7 @@ suite('ConfigurationModel', () => { }); test('inspect when raw is same', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, 'b': 1 }, keys: ['a'] }]); + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, 'b': 1 }, keys: ['a'] }], undefined, new NullLogService()); assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); @@ -363,7 +375,7 @@ suite('ConfigurationModel', () => { 'a': 2, 'b': 1 } - }]); + }], new NullLogService()); assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); @@ -373,8 +385,8 @@ suite('ConfigurationModel', () => { }); test('inspect in merged configuration when raw is same', () => { - const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }]); - const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], []); + const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], undefined, new NullLogService()); + const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, new NullLogService()); const testObject = target1.merge(target2); assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); @@ -393,8 +405,8 @@ suite('ConfigurationModel', () => { 'a': 2, 'b': 4, } - }]); - const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], []); + }], new NullLogService()); + const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, new NullLogService()); const testObject = target1.merge(target2); assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); @@ -409,7 +421,7 @@ suite('ConfigurationModel', () => { { identifiers: ['x', 'y'], contents: { 'a': 2, 'b': 1 }, keys: ['a', 'b'] }, { identifiers: ['x'], contents: { 'a': 3 }, keys: ['a'] }, { identifiers: ['y'], contents: { 'b': 3 }, keys: ['b'] } - ]); + ], undefined, new NullLogService()); assert.deepStrictEqual(testObject.inspect('a').overrides, [ { identifiers: ['x', 'y'], value: 2 }, @@ -418,7 +430,7 @@ suite('ConfigurationModel', () => { }); test('inspect when no overrides', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c']); + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [], undefined, new NullLogService()); assert.strictEqual(testObject.inspect('a').overrides, undefined); }); @@ -430,10 +442,10 @@ suite('CustomConfigurationModel', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('simple merge using models', () => { - const base = new ConfigurationModelParser('base'); + const base = new ConfigurationModelParser('base', new NullLogService()); base.parse(JSON.stringify({ 'a': 1, 'b': 2 })); - const add = new ConfigurationModelParser('add'); + const add = new ConfigurationModelParser('add', new NullLogService()); add.parse(JSON.stringify({ 'a': 3, 'c': 4 })); const result = base.configurationModel.merge(add.configurationModel); @@ -441,35 +453,35 @@ suite('CustomConfigurationModel', () => { }); test('simple merge with an undefined contents', () => { - let base = new ConfigurationModelParser('base'); + let base = new ConfigurationModelParser('base', new NullLogService()); base.parse(JSON.stringify({ 'a': 1, 'b': 2 })); - let add = new ConfigurationModelParser('add'); + let add = new ConfigurationModelParser('add', new NullLogService()); let result = base.configurationModel.merge(add.configurationModel); assert.deepStrictEqual(result.contents, { 'a': 1, 'b': 2 }); - base = new ConfigurationModelParser('base'); - add = new ConfigurationModelParser('add'); + base = new ConfigurationModelParser('base', new NullLogService()); + add = new ConfigurationModelParser('add', new NullLogService()); add.parse(JSON.stringify({ 'a': 1, 'b': 2 })); result = base.configurationModel.merge(add.configurationModel); assert.deepStrictEqual(result.contents, { 'a': 1, 'b': 2 }); - base = new ConfigurationModelParser('base'); - add = new ConfigurationModelParser('add'); + base = new ConfigurationModelParser('base', new NullLogService()); + add = new ConfigurationModelParser('add', new NullLogService()); result = base.configurationModel.merge(add.configurationModel); assert.deepStrictEqual(result.contents, {}); }); test('Recursive merge using config models', () => { - const base = new ConfigurationModelParser('base'); + const base = new ConfigurationModelParser('base', new NullLogService()); base.parse(JSON.stringify({ 'a': { 'b': 1 } })); - const add = new ConfigurationModelParser('add'); + const add = new ConfigurationModelParser('add', new NullLogService()); add.parse(JSON.stringify({ 'a': { 'b': 2 } })); const result = base.configurationModel.merge(add.configurationModel); assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 } }); }); test('Test contents while getting an existing property', () => { - const testObject = new ConfigurationModelParser('test'); + const testObject = new ConfigurationModelParser('test', new NullLogService()); testObject.parse(JSON.stringify({ 'a': 1 })); assert.deepStrictEqual(testObject.configurationModel.getValue('a'), 1); @@ -478,7 +490,7 @@ suite('CustomConfigurationModel', () => { }); test('Test contents are undefined for non existing properties', () => { - const testObject = new ConfigurationModelParser('test'); + const testObject = new ConfigurationModelParser('test', new NullLogService()); testObject.parse(JSON.stringify({ awesome: true })); @@ -487,26 +499,26 @@ suite('CustomConfigurationModel', () => { }); test('Test contents are undefined for undefined config', () => { - const testObject = new ConfigurationModelParser('test'); + const testObject = new ConfigurationModelParser('test', new NullLogService()); assert.deepStrictEqual(testObject.configurationModel.getValue('unknownproperty'), undefined); }); test('Test configWithOverrides gives all content merged with overrides', () => { - const testObject = new ConfigurationModelParser('test'); + const testObject = new ConfigurationModelParser('test', new NullLogService()); testObject.parse(JSON.stringify({ 'a': 1, 'c': 1, '[b]': { 'a': 2 } })); assert.deepStrictEqual(testObject.configurationModel.override('b').contents, { 'a': 2, 'c': 1, '[b]': { 'a': 2 } }); }); test('Test configWithOverrides gives empty contents', () => { - const testObject = new ConfigurationModelParser('test'); + const testObject = new ConfigurationModelParser('test', new NullLogService()); assert.deepStrictEqual(testObject.configurationModel.override('b').contents, {}); }); test('Test update with empty data', () => { - const testObject = new ConfigurationModelParser('test'); + const testObject = new ConfigurationModelParser('test', new NullLogService()); testObject.parse(''); assert.deepStrictEqual(testObject.configurationModel.contents, Object.create(null)); @@ -524,7 +536,7 @@ suite('CustomConfigurationModel', () => { }); test('Test empty property is not ignored', () => { - const testObject = new ConfigurationModelParser('test'); + const testObject = new ConfigurationModelParser('test', new NullLogService()); testObject.parse(JSON.stringify({ '': 1 })); // deepStrictEqual seems to ignore empty properties, fall back @@ -535,6 +547,30 @@ suite('CustomConfigurationModel', () => { }); +export class TestConfiguration extends Configuration { + + constructor( + defaultConfiguration: ConfigurationModel, + policyConfiguration: ConfigurationModel, + applicationConfiguration: ConfigurationModel, + localUserConfiguration: ConfigurationModel, + ) { + super( + defaultConfiguration, + policyConfiguration, + applicationConfiguration, + localUserConfiguration, + ConfigurationModel.createEmptyModel(new NullLogService()), + ConfigurationModel.createEmptyModel(new NullLogService()), + new ResourceMap(), + ConfigurationModel.createEmptyModel(new NullLogService()), + new ResourceMap(), + new NullLogService() + ); + } + +} + suite('Configuration', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -543,7 +579,7 @@ suite('Configuration', () => { const defaultConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 1 }, '[l2]': { 'b': 1 } }); const userConfigurationModel = parseConfigurationModel({ '[l3]': { 'a': 2 } }); const workspaceConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 3 }, '[l4]': { 'a': 3 } }); - const testObject: Configuration = new Configuration(defaultConfigurationModel, new ConfigurationModel(), userConfigurationModel, workspaceConfigurationModel); + const testObject: Configuration = new TestConfiguration(defaultConfigurationModel, ConfigurationModel.createEmptyModel(new NullLogService()), userConfigurationModel, workspaceConfigurationModel); const { overrideIdentifiers } = testObject.inspect('a', {}, undefined); @@ -551,9 +587,9 @@ suite('Configuration', () => { }); test('Test update value', () => { - const parser = new ConfigurationModelParser('test'); + const parser = new ConfigurationModelParser('test', new NullLogService()); parser.parse(JSON.stringify({ 'a': 1 })); - const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject: Configuration = new TestConfiguration(parser.configurationModel, ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.updateValue('a', 2); @@ -561,9 +597,9 @@ suite('Configuration', () => { }); test('Test update value after inspect', () => { - const parser = new ConfigurationModelParser('test'); + const parser = new ConfigurationModelParser('test', new NullLogService()); parser.parse(JSON.stringify({ 'a': 1 })); - const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject: Configuration = new TestConfiguration(parser.configurationModel, ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.inspect('a', {}, undefined); testObject.updateValue('a', 2); @@ -572,7 +608,7 @@ suite('Configuration', () => { }); test('Test compare and update default configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.updateDefaultConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on', })); @@ -589,7 +625,7 @@ suite('Configuration', () => { }); test('Test compare and update application configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.updateApplicationConfiguration(toConfigurationModel({ 'update.mode': 'on', })); @@ -606,7 +642,7 @@ suite('Configuration', () => { }); test('Test compare and update user configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.updateLocalUserConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -629,7 +665,7 @@ suite('Configuration', () => { }); test('Test compare and update workspace configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -652,7 +688,7 @@ suite('Configuration', () => { }); test('Test compare and update workspace folder configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -675,7 +711,7 @@ suite('Configuration', () => { }); test('Test compare and delete workspace folder configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const testObject = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); testObject.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -691,7 +727,7 @@ suite('Configuration', () => { }); function parseConfigurationModel(content: any): ConfigurationModel { - const parser = new ConfigurationModelParser('test'); + const parser = new ConfigurationModelParser('test', new NullLogService()); parser.parse(JSON.stringify(content)); return parser.configurationModel; } @@ -703,13 +739,13 @@ suite('ConfigurationChangeEvent', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('changeEvent affecting keys with new configuration', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 1, 'workbench.editor.enablePreview': false, 'files.autoSave': 'off', })); - const testObject = new ConfigurationChangeEvent(change, undefined, configuration); + const testObject = new ConfigurationChangeEvent(change, undefined, configuration, undefined, new NullLogService()); assert.deepStrictEqual([...testObject.affectedKeys], ['window.zoomLevel', 'workbench.editor.enablePreview', 'files.autoSave']); @@ -729,7 +765,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting keys when configuration changed', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); configuration.updateLocalUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 2, 'workbench.editor.enablePreview': true, @@ -741,7 +777,7 @@ suite('ConfigurationChangeEvent', () => { 'workbench.editor.enablePreview': false, 'files.autoSave': 'off', })); - const testObject = new ConfigurationChangeEvent(change, { data }, configuration); + const testObject = new ConfigurationChangeEvent(change, { data }, configuration, undefined, new NullLogService()); assert.deepStrictEqual([...testObject.affectedKeys], ['window.zoomLevel', 'workbench.editor.enablePreview']); @@ -758,7 +794,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting overrides with new configuration', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({ 'files.autoSave': 'off', '[markdown]': { @@ -768,7 +804,7 @@ suite('ConfigurationChangeEvent', () => { 'editor.lineNumbers': 'off' } })); - const testObject = new ConfigurationChangeEvent(change, undefined, configuration); + const testObject = new ConfigurationChangeEvent(change, undefined, configuration, undefined, new NullLogService()); assert.deepStrictEqual([...testObject.affectedKeys], ['files.autoSave', '[markdown]', '[typescript][jsonc]', 'editor.wordWrap', 'editor.lineNumbers']); @@ -800,7 +836,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting overrides when configuration changed', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); configuration.updateLocalUserConfiguration(toConfigurationModel({ 'workbench.editor.enablePreview': true, '[markdown]': { @@ -826,7 +862,7 @@ suite('ConfigurationChangeEvent', () => { }, 'window.zoomLevel': 1, })); - const testObject = new ConfigurationChangeEvent(change, { data }, configuration); + const testObject = new ConfigurationChangeEvent(change, { data }, configuration, undefined, new NullLogService()); assert.deepStrictEqual([...testObject.affectedKeys], ['window.zoomLevel', '[markdown]', '[css][scss]', 'workbench.editor.enablePreview', 'editor.fontSize', 'editor.lineNumbers']); @@ -867,7 +903,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting workspace folders', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); configuration.updateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' })); configuration.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true })); configuration.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true })); @@ -878,7 +914,7 @@ suite('ConfigurationChangeEvent', () => { configuration.compareAndUpdateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'window.zoomLevel': 1, 'window.restoreFullscreen': false })), configuration.compareAndUpdateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'workbench.editor.enablePreview': false, 'window.restoreWindows': false })) ); - const testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace); + const testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace, new NullLogService()); assert.deepStrictEqual([...testObject.affectedKeys], ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']); @@ -957,7 +993,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent - all', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); configuration.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true })); const data = configuration.toData(); const change = mergeChanges( @@ -976,7 +1012,7 @@ suite('ConfigurationChangeEvent', () => { configuration.compareAndDeleteFolderConfiguration(URI.file('file1')), configuration.compareAndUpdateFolderConfiguration(URI.file('file2'), toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true }))); const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('file1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('file2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]); - const testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace); + const testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace, new NullLogService()); assert.deepStrictEqual([...testObject.affectedKeys], ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows', 'editor.wordWrap']); @@ -1052,7 +1088,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting tasks and launches', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({ 'launch': { 'configuraiton': {} @@ -1062,7 +1098,7 @@ suite('ConfigurationChangeEvent', () => { 'version': 2 } })); - const testObject = new ConfigurationChangeEvent(change, undefined, configuration); + const testObject = new ConfigurationChangeEvent(change, undefined, configuration, undefined, new NullLogService()); assert.deepStrictEqual([...testObject.affectedKeys], ['launch', 'launch.version', 'tasks']); assert.ok(testObject.affectsConfiguration('launch')); @@ -1071,9 +1107,9 @@ suite('ConfigurationChangeEvent', () => { }); test('affectsConfiguration returns false for empty string', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); + const configuration = new TestConfiguration(ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService())); const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 1 })); - const testObject = new ConfigurationChangeEvent(change, undefined, configuration); + const testObject = new ConfigurationChangeEvent(change, undefined, configuration, undefined, new NullLogService()); assert.strictEqual(false, testObject.affectsConfiguration('')); }); @@ -1081,7 +1117,7 @@ suite('ConfigurationChangeEvent', () => { }); function toConfigurationModel(obj: any): ConfigurationModel { - const parser = new ConfigurationModelParser('test'); + const parser = new ConfigurationModelParser('test', new NullLogService()); parser.parse(JSON.stringify(obj)); return parser.configurationModel; } diff --git a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts index 9fc9e709322..0f3bf7e2d87 100644 --- a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts +++ b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,6 +14,14 @@ suite('ConfigurationRegistry', () => { const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + setup(() => reset()); + teardown(() => reset()); + + function reset() { + configurationRegistry.deregisterConfigurations(configurationRegistry.getConfigurations()); + configurationRegistry.deregisterDefaultConfigurations(configurationRegistry.getRegisteredDefaultConfigurations()); + } + test('configuration override', async () => { configurationRegistry.registerConfiguration({ 'id': '_test_default', @@ -31,6 +39,24 @@ suite('ConfigurationRegistry', () => { assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, c: 3 }); }); + test('configuration override defaults - prevent overriding default value', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config.preventDefaultValueOverride': { + 'type': 'object', + default: { a: 0 }, + 'disallowConfigurationDefault': true + } + } + }); + + configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config.preventDefaultValueOverride': { a: 1, b: 2 } } }]); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config.preventDefaultValueOverride'].default, { a: 0 }); + }); + test('configuration override defaults - merges defaults', async () => { configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 1, b: 2 } } }]); configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 2, c: 3 } } }]); @@ -38,7 +64,7 @@ suite('ConfigurationRegistry', () => { assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 }); }); - test('configuration defaults - overrides defaults', async () => { + test('configuration defaults - merge object default overrides', async () => { configurationRegistry.registerConfiguration({ 'id': '_test_default', 'type': 'object', @@ -51,7 +77,7 @@ suite('ConfigurationRegistry', () => { configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]); configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 2, c: 3 } } }]); - assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 }); + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); }); test('registering multiple settings with same policy', async () => { @@ -79,4 +105,88 @@ suite('ConfigurationRegistry', () => { assert.ok(actual['policy1'] !== undefined); assert.ok(actual['policy2'] === undefined); }); + + test('configuration defaults - deregister merged object default override', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config': { + 'type': 'object', + } + } + }); + + const overrides1 = [{ overrides: { 'config': { a: 1, b: 2 } }, source: { id: 'source1', displayName: 'source1' } }]; + const overrides2 = [{ overrides: { 'config': { a: 2, c: 3 } }, source: { id: 'source2', displayName: 'source2' } }]; + + configurationRegistry.registerDefaultConfigurations(overrides1); + configurationRegistry.registerDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides1); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, {}); + }); + + test('configuration defaults - deregister merged object default override without source', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config': { + 'type': 'object', + } + } + }); + + const overrides1 = [{ overrides: { 'config': { a: 1, b: 2 } } }]; + const overrides2 = [{ overrides: { 'config': { a: 2, c: 3 } } }]; + + configurationRegistry.registerDefaultConfigurations(overrides1); + configurationRegistry.registerDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides1); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, {}); + }); + + test('configuration defaults - deregister merged object default language overrides', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config': { + 'type': 'object', + } + } + }); + + const overrides1 = [{ overrides: { '[lang]': { 'config': { a: 1, b: 2 } } }, source: { id: 'source1', displayName: 'source1' } }]; + const overrides2 = [{ overrides: { '[lang]': { 'config': { a: 2, c: 3 } } }, source: { id: 'source2', displayName: 'source2' } }]; + + configurationRegistry.registerDefaultConfigurations(overrides1); + configurationRegistry.registerDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { 'config': { a: 2, b: 2, c: 3 } }); + + configurationRegistry.deregisterDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { 'config': { a: 1, b: 2 } }); + + configurationRegistry.deregisterDefaultConfigurations(overrides1); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'], undefined); + }); }); diff --git a/src/vs/platform/configuration/test/common/configurationService.test.ts b/src/vs/platform/configuration/test/common/configurationService.test.ts index 880e49e8e48..0eff504b9bd 100644 --- a/src/vs/platform/configuration/test/common/configurationService.test.ts +++ b/src/vs/platform/configuration/test/common/configurationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/configuration/test/common/configurations.test.ts b/src/vs/platform/configuration/test/common/configurations.test.ts index 63269f63ffa..5b237b73719 100644 --- a/src/vs/platform/configuration/test/common/configurations.test.ts +++ b/src/vs/platform/configuration/test/common/configurations.test.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { DefaultConfiguration } from 'vs/platform/configuration/common/configurations'; +import { NullLogService } from 'vs/platform/log/common/log'; import { Registry } from 'vs/platform/registry/common/platform'; suite('DefaultConfiguration', () => { @@ -21,12 +22,11 @@ suite('DefaultConfiguration', () => { function reset() { configurationRegistry.deregisterConfigurations(configurationRegistry.getConfigurations()); - const configurationDefaultsOverrides = configurationRegistry.getConfigurationDefaultsOverrides(); - configurationRegistry.deregisterDefaultConfigurations([...configurationDefaultsOverrides.keys()].map(key => ({ extensionId: configurationDefaultsOverrides.get(key)?.source, overrides: { [key]: configurationDefaultsOverrides.get(key)?.value } }))); + configurationRegistry.deregisterDefaultConfigurations(configurationRegistry.getRegisteredDefaultConfigurations()); } test('Test registering a property before initialize', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -45,7 +45,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering a property and do not initialize', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -63,7 +63,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering a property after initialize', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); await testObject.initialize(); const promise = Event.toPromise(testObject.onDidChangeConfiguration); configurationRegistry.registerConfiguration({ @@ -85,7 +85,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering nested properties', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -109,11 +109,11 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('a'), { b: { c: '2' } })); assert.ok(equals(actual.contents, { 'a': { b: { c: '2' } } })); - assert.deepStrictEqual(actual.keys, ['a.b', 'a.b.c']); + assert.deepStrictEqual(actual.keys.sort(), ['a.b', 'a.b.c']); }); test('Test registering the same property again', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -145,7 +145,7 @@ suite('DefaultConfiguration', () => { }); test('Test registering an override identifier', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); configurationRegistry.registerDefaultConfigurations([{ overrides: { '[a]': { @@ -157,12 +157,12 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); test('Test registering a normal property and override identifier', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -190,12 +190,12 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); test('Test normal property is registered after override identifier', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); const promise = Event.toPromise(testObject.onDidChangeConfiguration); configurationRegistry.registerDefaultConfigurations([{ overrides: { @@ -226,13 +226,13 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['[a]', 'b']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); assert.deepStrictEqual(properties, ['b']); }); test('Test override identifier is registered after property', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); const promise = Event.toPromise(testObject.onDidChangeConfiguration); configurationRegistry.registerConfiguration({ 'id': 'a', @@ -262,13 +262,13 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); assert.deepStrictEqual(properties, ['[a]']); }); test('Test register override identifier and property after initialize', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); await testObject.initialize(); @@ -298,12 +298,12 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); test('Test deregistering a property', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); const promise = Event.toPromise(testObject.onDidChangeConfiguration); const node: IConfigurationNode = { 'id': 'a', @@ -330,7 +330,7 @@ suite('DefaultConfiguration', () => { }); test('Test deregistering an override identifier', async () => { - const testObject = disposables.add(new DefaultConfiguration()); + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); configurationRegistry.registerConfiguration({ 'id': 'a', 'order': 1, @@ -360,4 +360,54 @@ suite('DefaultConfiguration', () => { assert.deepStrictEqual(testObject.configurationModel.keys, ['b']); assert.strictEqual(testObject.configurationModel.getOverrideValue('b', 'a'), undefined); }); + + test('Test deregistering a merged language object setting', async () => { + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); + configurationRegistry.registerConfiguration({ + 'id': 'b', + 'order': 1, + 'title': 'b', + 'type': 'object', + 'properties': { + 'b': { + 'description': 'b', + 'type': 'object', + 'default': {}, + } + } + }); + const node1 = { + overrides: { + '[a]': { + 'b': { + 'aa': '1', + 'bb': '2' + } + } + }, + source: { id: 'source1', displayName: 'source1' } + }; + + const node2 = { + overrides: { + '[a]': { + 'b': { + 'bb': '20', + 'cc': '30' + } + } + }, + source: { id: 'source2', displayName: 'source2' } + }; + configurationRegistry.registerDefaultConfigurations([node1]); + configurationRegistry.registerDefaultConfigurations([node2]); + await testObject.initialize(); + + configurationRegistry.deregisterDefaultConfigurations([node1]); + assert.ok(equals(testObject.configurationModel.getValue('[a]'), { 'b': { 'bb': '20', 'cc': '30' } })); + assert.ok(equals(testObject.configurationModel.contents, { '[a]': { 'b': { 'bb': '20', 'cc': '30' } }, 'b': {} })); + assert.ok(equals(testObject.configurationModel.overrides, [{ contents: { 'b': { 'bb': '20', 'cc': '30' } }, identifiers: ['a'], keys: ['b'] }])); + assert.deepStrictEqual(testObject.configurationModel.keys.sort(), ['[a]', 'b']); + assert.ok(equals(testObject.configurationModel.getOverrideValue('b', 'a'), { 'bb': '20', 'cc': '30' })); + }); }); diff --git a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts index e7c228e98fc..d3e44993618 100644 --- a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts +++ b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { DefaultConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; @@ -61,7 +61,7 @@ suite('PolicyConfiguration', () => { suiteTeardown(() => Registry.as(Extensions.Configuration).deregisterConfigurations([policyConfigurationNode])); setup(async () => { - const defaultConfiguration = disposables.add(new DefaultConfiguration()); + const defaultConfiguration = disposables.add(new DefaultConfiguration(new NullLogService())); await defaultConfiguration.initialize(); fileService = disposables.add(new FileService(new NullLogService())); const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); diff --git a/src/vs/platform/contextkey/test/browser/contextkey.test.ts b/src/vs/platform/contextkey/test/browser/contextkey.test.ts index b56d4b874da..d2301c19147 100644 --- a/src/vs/platform/contextkey/test/browser/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/browser/contextkey.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 8f388fb13ee..2555701c1d8 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ContextKeyExpr, ContextKeyExpression, implies } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/platform/contextkey/test/common/parser.test.ts b/src/vs/platform/contextkey/test/common/parser.test.ts index c5be2596341..17bfa468ec9 100644 --- a/src/vs/platform/contextkey/test/common/parser.test.ts +++ b/src/vs/platform/contextkey/test/common/parser.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Parser } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/platform/contextkey/test/common/scanner.test.ts b/src/vs/platform/contextkey/test/common/scanner.test.ts index df897db9e8a..dacbfbebbdd 100644 --- a/src/vs/platform/contextkey/test/common/scanner.test.ts +++ b/src/vs/platform/contextkey/test/common/scanner.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Scanner, Token, TokenType } from 'vs/platform/contextkey/common/scanner'; diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 55e0d50422d..abc08f92252 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; -import { $, addDisposableListener, EventType, getActiveElement, getWindow, isAncestor } from 'vs/base/browser/dom'; +import { $, addDisposableListener, EventType, getActiveElement, getWindow, isAncestor, isHTMLElement } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Menu } from 'vs/base/browser/ui/menu/menu'; import { ActionRunner, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; @@ -49,7 +49,7 @@ export class ContextMenuHandler { let menu: Menu | undefined; - const shadowRootElement = delegate.domForShadowRoot instanceof HTMLElement ? delegate.domForShadowRoot : undefined; + const shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined; this.contextViewService.showContextView({ getAnchor: () => delegate.getAnchor(), canRelayout: false, diff --git a/src/vs/platform/contextview/browser/contextMenuService.ts b/src/vs/platform/contextview/browser/contextMenuService.ts index 907cf9449a8..f70e349945e 100644 --- a/src/vs/platform/contextview/browser/contextMenuService.ts +++ b/src/vs/platform/contextview/browser/contextMenuService.ts @@ -86,9 +86,8 @@ export namespace ContextMenuMenuDelegate { getActions: () => { const target: IAction[] = []; if (menuId) { - const menu = menuService.createMenu(menuId, contextKeyService ?? globalContextKeyService); - createAndFillInContextMenuActions(menu, menuActionOptions, target); - menu.dispose(); + const menu = menuService.getMenuActions(menuId, contextKeyService ?? globalContextKeyService, menuActionOptions); + createAndFillInContextMenuActions(menu, target); } if (!delegate.getActions) { return target; diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index 87c58811d8d..7bd31c59886 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -19,7 +19,7 @@ export interface IContextViewService extends IContextViewProvider { readonly _serviceBrand: undefined; - showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable; + showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IOpenContextView; hideContextView(data?: any): void; getContextViewElement(): HTMLElement; layout(): void; @@ -48,6 +48,10 @@ export interface IContextViewDelegate { layer?: number; // Default: 0 } +export interface IOpenContextView { + close: () => void; +} + export const IContextMenuService = createDecorator('contextMenuService'); export interface IContextMenuService { diff --git a/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index beaf0b894e6..dee2144251e 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/src/vs/platform/contextview/browser/contextViewService.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { ContextView, ContextViewDOMPosition, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IContextViewDelegate, IContextViewService } from './contextView'; +import { IContextViewDelegate, IContextViewService, IOpenContextView } from './contextView'; import { getWindow } from 'vs/base/browser/dom'; - export class ContextViewHandler extends Disposable implements IContextViewProvider { - private currentViewDisposable = this._register(new MutableDisposable()); + private openContextView: IOpenContextView | undefined; protected readonly contextView = this._register(new ContextView(this.layoutService.mainContainer, ContextViewDOMPosition.ABSOLUTE)); constructor( @@ -26,7 +25,7 @@ export class ContextViewHandler extends Disposable implements IContextViewProvid // ContextView - showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable { + showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IOpenContextView { let domPosition: ContextViewDOMPosition; if (container) { if (container === this.layoutService.getContainer(getWindow(container))) { @@ -44,14 +43,16 @@ export class ContextViewHandler extends Disposable implements IContextViewProvid this.contextView.show(delegate); - const disposable = toDisposable(() => { - if (this.currentViewDisposable === disposable) { - this.hideContextView(); + const openContextView: IOpenContextView = { + close: () => { + if (this.openContextView === openContextView) { + this.hideContextView(); + } } - }); + }; - this.currentViewDisposable.value = disposable; - return disposable; + this.openContextView = openContextView; + return openContextView; } layout(): void { @@ -60,6 +61,7 @@ export class ContextViewHandler extends Disposable implements IContextViewProvid hideContextView(data?: any): void { this.contextView.hide(data); + this.openContextView = undefined; } } diff --git a/src/vs/platform/diagnostics/common/diagnostics.ts b/src/vs/platform/diagnostics/common/diagnostics.ts index 7d5af4f92da..fb30b762cef 100644 --- a/src/vs/platform/diagnostics/common/diagnostics.ts +++ b/src/vs/platform/diagnostics/common/diagnostics.ts @@ -80,6 +80,8 @@ export interface WorkspaceStats { fileCount: number; maxFilesReached: boolean; launchConfigFiles: WorkspaceStatItem[]; + totalScanTime: number; + totalReaddirCount: number; } export interface PerformanceInfo { diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 0be311f5051..7e0bc1170e0 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -2,6 +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 * as fs from 'fs'; import * as osLib from 'os'; import { Promises } from 'vs/base/common/async'; import { getNodeType, parse, ParseError } from 'vs/base/common/json'; @@ -9,6 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import { basename, join } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { ProcessItem } from 'vs/base/common/processes'; +import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import { virtualMachineHint } from 'vs/base/node/id'; import { IDirent, Promises as pfs } from 'vs/base/node/pfs'; @@ -60,11 +63,13 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P const MAX_FILES = 20000; - function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean }): Promise { + function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean; readdirCount: number }): Promise { const relativePath = dir.substring(root.length + 1); return Promises.withAsyncBody(async resolve => { let files: IDirent[]; + + token.readdirCount++; try { files = await pfs.readdir(dir, { withFileTypes: true }); } catch (error) { @@ -130,8 +135,8 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P } const statsPromise = Promises.withAsyncBody(async (resolve) => { - const token: { count: number; maxReached: boolean } = { count: 0, maxReached: false }; - + const token: { count: number; maxReached: boolean; readdirCount: number } = { count: 0, maxReached: false, readdirCount: 0 }; + const sw = new StopWatch(true); await collect(folder, folder, filter, token); const launchConfigs = await collectLaunchConfigs(folder); resolve({ @@ -139,7 +144,9 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P fileTypes: asSortedItems(fileTypes), fileCount: token.count, maxFilesReached: token.maxReached, - launchConfigFiles: launchConfigs + launchConfigFiles: launchConfigs, + totalScanTime: sw.elapsed(), + totalReaddirCount: token.readdirCount }); }); @@ -173,7 +180,7 @@ export async function collectLaunchConfigs(folder: string): Promise(); const launchConfig = join(folder, '.vscode', 'launch.json'); - const contents = await pfs.readFile(launchConfig); + const contents = await fs.promises.readFile(launchConfig); const errors: ParseError[] = []; const json = parse(contents.toString(), errors); @@ -539,8 +546,8 @@ export class DiagnosticsService implements IDiagnosticsService { owner: 'lramos15'; comment: 'Helps us gain insights into what type of files are being used in a workspace'; rendererSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the session.' }; - type: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The type of file' }; - count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'How many types of that file are present' }; + type: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of file' }; + count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How many types of that file are present' }; }; type WorkspaceStatsFileEvent = { rendererSessionId: string; @@ -568,6 +575,23 @@ export class DiagnosticsService implements IDiagnosticsService { count: e.count }); }); + + // Workspace stats metadata + type WorkspaceStatsMetadataClassification = { + owner: 'jrieken'; + comment: 'Metadata about workspace metadata collection'; + duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How did it take to make workspace stats' }; + reachedLimit: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Did making workspace stats reach its limits' }; + fileCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many files did workspace stats discover' }; + readdirCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many readdir call were needed' }; + }; + type WorkspaceStatsMetadata = { + duration: number; + reachedLimit: boolean; + fileCount: number; + readdirCount: number; + }; + this.telemetryService.publicLog2('workspace.stats.metadata', { duration: stats.totalScanTime, reachedLimit: stats.maxFilesReached, fileCount: stats.fileCount, readdirCount: stats.totalReaddirCount }); } catch { // Report nothing if collecting metadata fails. } diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 666829d56be..2fffe78c5fb 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, dialog, FileFilter, MessageBoxOptions, MessageBoxReturnValue, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'; +import electron from 'electron'; import { Queue } from 'vs/base/common/async'; import { hash } from 'vs/base/common/hash'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -24,14 +24,14 @@ export interface IDialogMainService { readonly _serviceBrand: undefined; - pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; - pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; - pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; - pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; + pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; + pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; + pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; + pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; - showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise; - showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise; - showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise; + showMessageBox(options: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise; + showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise; + showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise; } interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions { @@ -40,7 +40,7 @@ interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions { readonly title: string; readonly buttonLabel?: string; - readonly filters?: FileFilter[]; + readonly filters?: electron.FileFilter[]; } export class DialogMainService implements IDialogMainService { @@ -48,8 +48,8 @@ export class DialogMainService implements IDialogMainService { declare readonly _serviceBrand: undefined; private readonly windowFileDialogLocks = new Map>(); - private readonly windowDialogQueues = new Map>(); - private readonly noWindowDialogueQueue = new Queue(); + private readonly windowDialogQueues = new Map>(); + private readonly noWindowDialogueQueue = new Queue(); constructor( @ILogService private readonly logService: ILogService, @@ -57,19 +57,19 @@ export class DialogMainService implements IDialogMainService { ) { } - pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', "Open") }, window); } - pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', "Open Folder") }, window); } - pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window); } - pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { const title = localize('openWorkspaceTitle', "Open Workspace from File"); const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")); const filters = WORKSPACE_FILTER; @@ -77,10 +77,10 @@ export class DialogMainService implements IDialogMainService { return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window); } - private async doPick(options: IInternalNativeOpenDialogOptions, window?: BrowserWindow): Promise { + private async doPick(options: IInternalNativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { // Ensure dialog options - const dialogOptions: OpenDialogOptions = { + const dialogOptions: electron.OpenDialogOptions = { title: options.title, buttonLabel: options.buttonLabel, filters: options.filters, @@ -105,7 +105,7 @@ export class DialogMainService implements IDialogMainService { } // Show Dialog - const result = await this.showOpenDialog(dialogOptions, (window || BrowserWindow.getFocusedWindow()) ?? undefined); + const result = await this.showOpenDialog(dialogOptions, (window || electron.BrowserWindow.getFocusedWindow()) ?? undefined); if (result && result.filePaths && result.filePaths.length > 0) { return result.filePaths; } @@ -113,14 +113,14 @@ export class DialogMainService implements IDialogMainService { return undefined; } - private getWindowDialogQueue(window?: BrowserWindow): Queue { + private getWindowDialogQueue(window?: electron.BrowserWindow): Queue { // Queue message box requests per window so that one can show // after the other. if (window) { let windowDialogQueue = this.windowDialogQueues.get(window.id); if (!windowDialogQueue) { - windowDialogQueue = new Queue(); + windowDialogQueue = new Queue(); this.windowDialogQueues.set(window.id, windowDialogQueue); } @@ -130,15 +130,15 @@ export class DialogMainService implements IDialogMainService { } } - showMessageBox(rawOptions: MessageBoxOptions, window?: BrowserWindow): Promise { - return this.getWindowDialogQueue(window).queue(async () => { + showMessageBox(rawOptions: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise { + return this.getWindowDialogQueue(window).queue(async () => { const { options, buttonIndeces } = massageMessageBoxOptions(rawOptions, this.productService); - let result: MessageBoxReturnValue | undefined = undefined; + let result: electron.MessageBoxReturnValue | undefined = undefined; if (window) { - result = await dialog.showMessageBox(window, options); + result = await electron.dialog.showMessageBox(window, options); } else { - result = await dialog.showMessageBox(options); + result = await electron.dialog.showMessageBox(options); } return { @@ -148,23 +148,23 @@ export class DialogMainService implements IDialogMainService { }); } - async showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise { + async showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise { // Prevent duplicates of the same dialog queueing at the same time const fileDialogLock = this.acquireFileDialogLock(options, window); if (!fileDialogLock) { this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration'); - return { canceled: true }; + return { canceled: true, filePath: '' }; } try { - return await this.getWindowDialogQueue(window).queue(async () => { - let result: SaveDialogReturnValue; + return await this.getWindowDialogQueue(window).queue(async () => { + let result: electron.SaveDialogReturnValue; if (window) { - result = await dialog.showSaveDialog(window, options); + result = await electron.dialog.showSaveDialog(window, options); } else { - result = await dialog.showSaveDialog(options); + result = await electron.dialog.showSaveDialog(options); } result.filePath = this.normalizePath(result.filePath); @@ -190,7 +190,7 @@ export class DialogMainService implements IDialogMainService { return paths.map(path => this.normalizePath(path)); } - async showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { + async showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise { // Ensure the path exists (if provided) if (options.defaultPath) { @@ -209,12 +209,12 @@ export class DialogMainService implements IDialogMainService { } try { - return await this.getWindowDialogQueue(window).queue(async () => { - let result: OpenDialogReturnValue; + return await this.getWindowDialogQueue(window).queue(async () => { + let result: electron.OpenDialogReturnValue; if (window) { - result = await dialog.showOpenDialog(window, options); + result = await electron.dialog.showOpenDialog(window, options); } else { - result = await dialog.showOpenDialog(options); + result = await electron.dialog.showOpenDialog(options); } result.filePaths = this.normalizePaths(result.filePaths); @@ -226,7 +226,7 @@ export class DialogMainService implements IDialogMainService { } } - private acquireFileDialogLock(options: SaveDialogOptions | OpenDialogOptions, window?: BrowserWindow): IDisposable | undefined { + private acquireFileDialogLock(options: electron.SaveDialogOptions | electron.OpenDialogOptions, window?: electron.BrowserWindow): IDisposable | undefined { // If no window is provided, allow as many dialogs as // needed since we consider them not modal per window diff --git a/src/vs/platform/dnd/browser/dnd.ts b/src/vs/platform/dnd/browser/dnd.ts index 1bd55afefaf..b742b3ae448 100644 --- a/src/vs/platform/dnd/browser/dnd.ts +++ b/src/vs/platform/dnd/browser/dnd.ts @@ -201,6 +201,7 @@ async function extractFilesDropData(accessor: ServicesAccessor, event: DragEvent async function extractFileTransferData(accessor: ServicesAccessor, items: DataTransferItemList): Promise { const fileSystemProvider = accessor.get(IFileService).getProvider(Schemas.file); + // eslint-disable-next-line no-restricted-syntax if (!(fileSystemProvider instanceof HTMLFileSystemProvider)) { return []; // only supported when running in web } diff --git a/src/vs/platform/encryption/electron-main/encryptionMainService.ts b/src/vs/platform/encryption/electron-main/encryptionMainService.ts index c937778c08d..423f0a2b363 100644 --- a/src/vs/platform/encryption/electron-main/encryptionMainService.ts +++ b/src/vs/platform/encryption/electron-main/encryptionMainService.ts @@ -25,12 +25,14 @@ export class EncryptionMainService implements IEncryptionMainService { ) { // if this commandLine switch is set, the user has opted in to using basic text encryption if (app.commandLine.getSwitchValue('password-store') === PasswordStoreCLIOption.basic) { + this.logService.trace('[EncryptionMainService] setting usePlainTextEncryption to true...'); safeStorage.setUsePlainTextEncryption?.(true); + this.logService.trace('[EncryptionMainService] set usePlainTextEncryption to true'); } } async encrypt(value: string): Promise { - this.logService.trace('[EncryptionMainService] Encrypting value.'); + this.logService.trace('[EncryptionMainService] Encrypting value...'); try { const result = JSON.stringify(safeStorage.encryptString(value)); this.logService.trace('[EncryptionMainService] Encrypted value.'); @@ -50,7 +52,7 @@ export class EncryptionMainService implements IEncryptionMainService { } const bufferToDecrypt = Buffer.from(parsedValue.data); - this.logService.trace('[EncryptionMainService] Decrypting value.'); + this.logService.trace('[EncryptionMainService] Decrypting value...'); const result = safeStorage.decryptString(bufferToDecrypt); this.logService.trace('[EncryptionMainService] Decrypted value.'); return result; @@ -61,7 +63,10 @@ export class EncryptionMainService implements IEncryptionMainService { } isEncryptionAvailable(): Promise { - return Promise.resolve(safeStorage.isEncryptionAvailable()); + this.logService.trace('[EncryptionMainService] Checking if encryption is available...'); + const result = safeStorage.isEncryptionAvailable(); + this.logService.trace('[EncryptionMainService] Encryption is available: ', result); + return Promise.resolve(result); } getKeyStorageProvider(): Promise { @@ -73,7 +78,9 @@ export class EncryptionMainService implements IEncryptionMainService { } if (safeStorage.getSelectedStorageBackend) { try { + this.logService.trace('[EncryptionMainService] Getting selected storage backend...'); const result = safeStorage.getSelectedStorageBackend() as KnownStorageProvider; + this.logService.trace('[EncryptionMainService] Selected storage backend: ', result); return Promise.resolve(result); } catch (e) { this.logService.error(e); @@ -95,6 +102,8 @@ export class EncryptionMainService implements IEncryptionMainService { throw new Error('Setting plain text encryption is not supported.'); } + this.logService.trace('[EncryptionMainService] Setting usePlainTextEncryption to true...'); safeStorage.setUsePlainTextEncryption(true); + this.logService.trace('[EncryptionMainService] Set usePlainTextEncryption to true'); } } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 18d4fcd795a..818021ff0c1 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -38,7 +38,6 @@ export interface NativeParsedArgs { add?: boolean; goto?: boolean; 'new-window'?: boolean; - 'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch. 'reuse-window'?: boolean; locale?: string; 'user-data-dir'?: string; @@ -130,6 +129,7 @@ export interface NativeParsedArgs { 'inspect'?: string; 'inspect-brk'?: string; 'js-flags'?: string; + 'disable-lcd-text'?: boolean; 'disable-gpu'?: boolean; 'disable-gpu-sandbox'?: boolean; 'nolazy'?: boolean; @@ -141,4 +141,8 @@ export interface NativeParsedArgs { 'vmodule'?: string; 'disable-dev-shm-usage'?: boolean; 'ozone-platform'?: string; + 'enable-tracing'?: string; + 'trace-startup-format'?: string; + 'trace-startup-file'?: string; + 'trace-startup-duration'?: string; } diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 748ff075783..dec04406afa 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -19,9 +19,6 @@ export const IEnvironmentMainService = refineServiceDecorator = {}; - @memoize - get cachedLanguagesPath(): string { return join(this.userDataPath, 'clp'); } - @memoize get backupHome(): string { return join(this.userDataPath, 'Backups'); } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 327c18c18ad..16a942afe05 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as minimist from 'minimist'; +import minimist from 'minimist'; import { isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; @@ -120,6 +120,7 @@ export const OPTIONS: OptionDescriptions> = { 'inspect-extensions': { type: 'string', allowEmptyValue: true, deprecates: ['debugPluginHost'], args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', allowEmptyValue: true, deprecates: ['debugBrkPluginHost'], args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, + 'disable-lcd-text': { type: 'boolean', cat: 't', description: localize('disableLCDText', "Disable LCD font rendering.") }, 'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") }, 'disable-chromium-sandbox': { type: 'boolean', cat: 't', description: localize('disableChromiumSandbox', "Use this option only when there is requirement to launch the application as sudo user on Linux or when running as an elevated user in an applocker environment on Windows.") }, 'sandbox': { type: 'boolean' }, @@ -157,7 +158,6 @@ export const OPTIONS: OptionDescriptions> = { 'crash-reporter-directory': { type: 'string' }, 'crash-reporter-id': { type: 'string' }, 'skip-add-to-recently-opened': { type: 'boolean' }, - 'unity-launch': { type: 'boolean' }, 'open-url': { type: 'boolean' }, 'file-write': { type: 'boolean' }, 'file-chmod': { type: 'boolean' }, @@ -205,6 +205,10 @@ export const OPTIONS: OptionDescriptions> = { 'disable-dev-shm-usage': { type: 'boolean' }, 'profile-temp': { type: 'boolean' }, 'ozone-platform': { type: 'string' }, + 'enable-tracing': { type: 'string' }, + 'trace-startup-format': { type: 'string' }, + 'trace-startup-file': { type: 'string' }, + 'trace-startup-duration': { type: 'string' }, _: { type: 'string[]' } // main arguments }; diff --git a/src/vs/platform/environment/node/argvHelper.ts b/src/vs/platform/environment/node/argvHelper.ts index d8cefb6df67..a94fca911ea 100644 --- a/src/vs/platform/environment/node/argvHelper.ts +++ b/src/vs/platform/environment/node/argvHelper.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; diff --git a/src/vs/platform/environment/node/stdin.ts b/src/vs/platform/environment/node/stdin.ts index 56ab151407d..b3e71e04493 100644 --- a/src/vs/platform/environment/node/stdin.ts +++ b/src/vs/platform/environment/node/stdin.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { tmpdir } from 'os'; import { Queue } from 'vs/base/common/async'; import { randomPath } from 'vs/base/common/extpath'; -import { Promises } from 'vs/base/node/pfs'; import { resolveTerminalEncoding } from 'vs/base/node/terminalEncoding'; export function hasStdinWithoutTty() { @@ -43,7 +43,7 @@ export async function readFromStdin(targetPath: string, verbose: boolean, onEnd? let [encoding, iconv] = await Promise.all([ resolveTerminalEncoding(verbose), // respect terminal encoding when piping into file import('@vscode/iconv-lite-umd'), // lazy load encoding module for usage - Promises.appendFile(targetPath, '') // make sure file exists right away (https://github.com/microsoft/vscode/issues/155341) + fs.promises.appendFile(targetPath, '') // make sure file exists right away (https://github.com/microsoft/vscode/issues/155341) ]); if (!iconv.encodingExists(encoding)) { @@ -63,7 +63,7 @@ export async function readFromStdin(targetPath: string, verbose: boolean, onEnd? process.stdin.on('data', chunk => { const chunkStr = decoder.write(chunk); - appendFileQueue.queue(() => Promises.appendFile(targetPath, chunkStr)); + appendFileQueue.queue(() => fs.promises.appendFile(targetPath, chunkStr)); }); process.stdin.on('end', () => { @@ -72,7 +72,7 @@ export async function readFromStdin(targetPath: string, verbose: boolean, onEnd? appendFileQueue.queue(async () => { try { if (typeof end === 'string') { - await Promises.appendFile(targetPath, end); + await fs.promises.appendFile(targetPath, end); } } finally { onEnd?.(); diff --git a/src/vs/platform/environment/node/userDataPath.js b/src/vs/platform/environment/node/userDataPath.js index 92898523ed1..3661c50a3ba 100644 --- a/src/vs/platform/environment/node/userDataPath.js +++ b/src/vs/platform/environment/node/userDataPath.js @@ -6,12 +6,22 @@ /// //@ts-check +'use strict'; + +// ESM-uncomment-begin +// import * as os from 'os'; +// import * as path from 'path'; +// +// const module = { exports: {} }; +// ESM-uncomment-end + (function () { - 'use strict'; /** - * @typedef {import('../../environment/common/argv').NativeParsedArgs} NativeParsedArgs - * + * @import { NativeParsedArgs } from '../../environment/common/argv' + */ + + /** * @param {typeof import('path')} path * @param {typeof import('os')} os * @param {string} cwd @@ -115,11 +125,17 @@ return factory(path, os, process.cwd()); // amd }); } else if (typeof module === 'object' && typeof module.exports === 'object') { + // ESM-comment-begin const path = require('path'); const os = require('os'); + // ESM-comment-end module.exports = factory(path, os, process.env['VSCODE_CWD'] || process.cwd()); // commonjs } else { throw new Error('Unknown context'); } }()); + +// ESM-uncomment-begin +// export const getUserDataPath = module.exports.getUserDataPath; +// ESM-uncomment-end diff --git a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts index 78fd7354520..268f5ce52bb 100644 --- a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts +++ b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import product from 'vs/platform/product/common/product'; import { isLinux } from 'vs/base/common/platform'; diff --git a/src/vs/platform/environment/test/node/argv.test.ts b/src/vs/platform/environment/test/node/argv.test.ts index a188b52b711..a82be9607d0 100644 --- a/src/vs/platform/environment/test/node/argv.test.ts +++ b/src/vs/platform/environment/test/node/argv.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { formatOptions, Option, OptionDescriptions, Subcommand, parseArgs, ErrorReporter } from 'vs/platform/environment/node/argv'; import { addArg } from 'vs/platform/environment/node/argvHelper'; diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index ffe418fc702..6f256621040 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseExtensionHostDebugPort } from 'vs/platform/environment/common/environmentService'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 68128a214fd..fb0033b2ada 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { flakySuite } from 'vs/base/test/common/testUtils'; @@ -19,7 +19,7 @@ flakySuite('Native Modules (all platforms)', () => { }); test('native-is-elevated', async () => { - const isElevated = await import('native-is-elevated'); + const isElevated = (await import('native-is-elevated')).default; assert.ok(typeof isElevated === 'function', testErrorMessage('native-is-elevated ')); const result = isElevated(); @@ -56,7 +56,12 @@ flakySuite('Native Modules (all platforms)', () => { }); test('@vscode/sqlite3', async () => { + // ESM-comment-begin const sqlite3 = await import('@vscode/sqlite3'); + // ESM-comment-end + // ESM-uncomment-begin + // const { default: sqlite3 } = await import('@vscode/sqlite3'); + // ESM-uncomment-end assert.ok(typeof sqlite3.Database === 'function', testErrorMessage('@vscode/sqlite3')); }); @@ -113,21 +118,18 @@ flakySuite('Native Modules (all platforms)', () => { assert.ok(typeof result === 'string' || typeof result === 'undefined', testErrorMessage('@vscode/windows-registry')); }); - test('@vscode/windows-ca-certs', async () => { - // @ts-ignore we do not directly depend on this module anymore - // but indirectly from our dependency to `@vscode/proxy-agent` - // we still want to ensure this module can work properly. - const windowsCerts = await import('@vscode/windows-ca-certs'); - const store = new windowsCerts.Crypt32(); - assert.ok(windowsCerts, testErrorMessage('@vscode/windows-ca-certs')); - let certCount = 0; - try { - while (store.next()) { - certCount++; + test('@vscode/proxy-agent', async () => { + const proxyAgent = await import('@vscode/proxy-agent'); + // This call will load `@vscode/proxy-agent` which is a native module that we want to test on Windows + const windowsCerts = await proxyAgent.loadSystemCertificates({ + log: { + trace: () => { }, + debug: () => { }, + info: () => { }, + warn: () => { }, + error: () => { } } - } finally { - store.done(); - } - assert(certCount > 0); + }); + assert.ok(windowsCerts.length > 0, testErrorMessage('@vscode/proxy-agent')); }); }); diff --git a/src/vs/platform/environment/test/node/userDataPath.test.ts b/src/vs/platform/environment/test/node/userDataPath.test.ts index 644260cff8f..72278e46ac0 100644 --- a/src/vs/platform/environment/test/node/userDataPath.test.ts +++ b/src/vs/platform/environment/test/node/userDataPath.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; import { getUserDataPath } from 'vs/platform/environment/node/userDataPath'; diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 5360015fbfb..42f230be716 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -6,9 +6,10 @@ import { distinct, isNonEmptyArray } from 'vs/base/common/arrays'; import { Barrier, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; +import { CancellationError, getErrorMessage, isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { isWeb } from 'vs/base/common/platform'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -17,10 +18,14 @@ import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, - IProductVersion + IProductVersion, ExtensionGalleryErrorCode, + EXTENSION_INSTALL_SOURCE_CONTEXT, + DidUpdateExtensionMetadata, + UninstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -32,10 +37,10 @@ export type InstallableExtension = { readonly manifest: IExtensionManifest; exte export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI; readonly productVersion: IProductVersion }; export interface IInstallExtensionTask { + readonly manifest: IExtensionManifest; readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; readonly operation: InstallOperation; - readonly profileLocation: URI; readonly options: InstallExtensionTaskOptions; readonly verificationStatus?: ExtensionVerificationStatus; run(): Promise; @@ -45,6 +50,7 @@ export interface IInstallExtensionTask { export type UninstallExtensionTaskOptions = UninstallOptions & { readonly profileLocation: URI }; export interface IUninstallExtensionTask { + readonly options: UninstallExtensionTaskOptions; readonly extension: ILocalExtension; run(): Promise; waitUntilTaskIsFinished(): Promise; @@ -72,7 +78,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected _onDidUninstallExtension = this._register(new Emitter()); get onDidUninstallExtension() { return this._onDidUninstallExtension.event; } - protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); + protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; } private readonly participants: IExtensionManagementParticipant[] = []; @@ -128,7 +134,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date }); installableExtensions.push({ ...compatible, options }); } catch (error) { - results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); + results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error, profileLocation: options.profileLocation ?? this.getCurrentExtensionsManifestLocation() }); } })); @@ -136,22 +142,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl results.push(...await this.installExtensions(installableExtensions)); } - for (const result of results) { - if (result.error) { - this.logService.error(`Failed to install extension.`, result.identifier.id); - this.logService.error(result.error); - if (result.source && !URI.isUri(result.source)) { - reportTelemetry(this.telemetryService, 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(result.source), error: result.error }); - } - } - } - return results; } - async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise { + async uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id); - return this.uninstallExtension(extension, options); + return this.uninstallExtensions([{ extension, options }]); } async toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise { @@ -169,7 +165,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const existing = (await this.getInstalled(ExtensionType.User, profile.extensionsResource)) .find(e => areSameExtensions(e.identifier, extension.identifier)); if (existing) { - this._onDidUpdateExtensionMetadata.fire(existing); + this._onDidUpdateExtensionMetadata.fire({ local: existing, profileLocation: profile.extensionsResource }); } else { this._onDidUninstallExtension.fire({ identifier: extension.identifier, profileLocation: profile.extensionsResource }); } @@ -203,20 +199,36 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.participants.push(participant); } - protected async installExtensions(extensions: InstallableExtension[]): Promise { - const results: InstallExtensionResult[] = []; + async resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { + try { + await this.joinAllSettled(this.userDataProfilesService.profiles.map( + async profile => { + const extensions = await this.getInstalled(ExtensionType.User, profile.extensionsResource); + await this.joinAllSettled(extensions.map( + async extension => { + if (extension.pinned !== pinned) { + await this.updateMetadata(extension, { pinned }, profile.extensionsResource); + } + })); + })); + } catch (error) { + this.logService.error('Error while resetting pinned state for all user extensions', getErrorMessage(error)); + throw error; + } + } - const installingExtensionsMap = new Map(); + protected async installExtensions(extensions: InstallableExtension[]): Promise { + const installExtensionResultsMap = new Map(); + const installingExtensionsMap = new Map(); const alreadyRequestedInstallations: Promise[] = []; - const successResults: (InstallExtensionResult & { local: ILocalExtension; profileLocation: URI })[] = []; const getInstallExtensionTaskKey = (extension: IGalleryExtension, profileLocation: URI) => `${ExtensionKey.create(extension).toString()}-${profileLocation.toString()}`; - const createInstallExtensionTask = (manifest: IExtensionManifest, extension: IGalleryExtension | URI, options: InstallExtensionTaskOptions): void => { + const createInstallExtensionTask = (manifest: IExtensionManifest, extension: IGalleryExtension | URI, options: InstallExtensionTaskOptions, root: IInstallExtensionTask | undefined): void => { const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options); - const key = URI.isUri(extension) ? extension.path : `${extension.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`; - installingExtensionsMap.set(key, { task: installExtensionTask, manifest }); + const key = `${getGalleryExtensionId(manifest.publisher, manifest.name)}-${options.profileLocation.toString()}`; + installingExtensionsMap.set(key, { task: installExtensionTask, root }); this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation }); - this.logService.info('Installing extension:', installExtensionTask.identifier.id); + this.logService.info('Installing extension:', installExtensionTask.identifier.id, options); // only cache gallery extensions tasks if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension, options.profileLocation), { task: installExtensionTask, waitingTasks: [] }); @@ -237,22 +249,22 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined; if (existingInstallExtensionTask) { - this.logService.info('Extension is already requested to install', existingInstallExtensionTask.task.identifier.id); + this.logService.info('Extension is already requested to install', existingInstallExtensionTask.task.identifier.id, installExtensionTaskOptions.profileLocation.toString()); alreadyRequestedInstallations.push(existingInstallExtensionTask.task.waitUntilTaskIsFinished()); } else { - createInstallExtensionTask(manifest, extension, installExtensionTaskOptions); + createInstallExtensionTask(manifest, extension, installExtensionTaskOptions, undefined); } } // collect and start installing all dependencies and pack extensions - await Promise.all([...installingExtensionsMap.values()].map(async ({ task, manifest }) => { + await Promise.all([...installingExtensionsMap.values()].map(async ({ task }) => { if (task.options.donotIncludePackAndDependencies) { this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion); + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, task.manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion); const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion); - const options: InstallExtensionTaskOptions = { ...task.options, donotIncludePackAndDependencies: true, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; + const options: InstallExtensionTaskOptions = { ...task.options, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { if (installingExtensionsMap.has(`${gallery.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`)) { continue; @@ -261,14 +273,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl if (existingInstallingExtension) { if (this.canWaitForTask(task, existingInstallingExtension.task)) { const identifier = existingInstallingExtension.task.identifier; - this.logService.info('Waiting for already requested installing extension', identifier.id, task.identifier.id); + this.logService.info('Waiting for already requested installing extension', identifier.id, task.identifier.id, options.profileLocation.toString()); existingInstallingExtension.waitingTasks.push(task); // add promise that waits until the extension is completely installed, ie., onDidInstallExtensions event is triggered for this extension alreadyRequestedInstallations.push( Event.toPromise( Event.filter(this.onDidInstallExtensions, results => results.some(result => areSameExtensions(result.identifier, identifier))) ).then(results => { - this.logService.info('Finished waiting for already requested installing extension', identifier.id, task.identifier.id); + this.logService.info('Finished waiting for already requested installing extension', identifier.id, task.identifier.id, options.profileLocation.toString()); const result = results.find(result => areSameExtensions(result.identifier, identifier)); if (!result?.local) { // Extension failed to install @@ -277,17 +289,17 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } } else if (!installed.some(({ identifier }) => areSameExtensions(identifier, gallery.identifier))) { - createInstallExtensionTask(manifest, gallery, options); + createInstallExtensionTask(manifest, gallery, options, task); } } } catch (error) { // Installing through VSIX if (URI.isUri(task.source)) { // Ignore installing dependencies and packs - if (isNonEmptyArray(manifest.extensionDependencies)) { + if (isNonEmptyArray(task.manifest.extensionDependencies)) { this.logService.warn(`Cannot install dependencies of extension:`, task.identifier.id, error.message); } - if (isNonEmptyArray(manifest.extensionPack)) { + if (isNonEmptyArray(task.manifest.extensionPack)) { this.logService.warn(`Cannot install packed extensions of extension:`, task.identifier.id, error.message); } } else { @@ -299,73 +311,126 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); // Install extensions in parallel and wait until all extensions are installed / failed - await this.joinAllSettled([...installingExtensionsMap.values()].map(async ({ task }) => { + await this.joinAllSettled([...installingExtensionsMap.entries()].map(async ([key, { task }]) => { const startTime = new Date().getTime(); + let local: ILocalExtension; try { - const local = await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None))); + local = await task.run(); + await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None)), ExtensionManagementErrorCode.PostInstall); + } catch (e) { + const error = toExtensionManagementError(e); if (!URI.isUri(task.source)) { - const isUpdate = task.operation === InstallOperation.Update; - const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; - reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), - verificationStatus: task.verificationStatus, - duration: new Date().getTime() - startTime, - durationSinceUpdate + error, + source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] }); - // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. - if (isWeb && task.operation !== InstallOperation.Update) { - try { - await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); - } catch (error) { /* ignore */ } - } } - - successResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); - } catch (error) { - this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error)); + installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: task.options.isApplicationScoped }); + this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error), task.options.profileLocation.toString()); throw error; } + if (!URI.isUri(task.source)) { + const isUpdate = task.operation === InstallOperation.Update; + const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; + reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + verificationStatus: task.verificationStatus, + duration: new Date().getTime() - startTime, + durationSinceUpdate, + source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] + }); + // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. + if (isWeb && task.operation !== InstallOperation.Update) { + try { + await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); + } catch (error) { /* ignore */ } + } + } + installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: local.isApplicationScoped }); })); if (alreadyRequestedInstallations.length) { await this.joinAllSettled(alreadyRequestedInstallations); } - - for (const result of successResults) { - this.logService.info(`Extension installed successfully:`, result.identifier.id); - results.push(result); - } - return results; + return [...installExtensionResultsMap.values()]; } catch (error) { - // rollback installed extensions - if (successResults.length) { - this.logService.info('Rollback: Uninstalling installed extensions', getErrorMessage(error)); - await Promise.allSettled(successResults.map(async ({ local, profileLocation }) => { + const getAllDepsAndPacks = (extension: ILocalExtension, profileLocation: URI, allDepsOrPacks: string[]) => { + const depsOrPacks = []; + if (extension.manifest.extensionDependencies?.length) { + depsOrPacks.push(...extension.manifest.extensionDependencies); + } + if (extension.manifest.extensionPack?.length) { + depsOrPacks.push(...extension.manifest.extensionPack); + } + for (const id of depsOrPacks) { + if (allDepsOrPacks.includes(id.toLowerCase())) { + continue; + } + allDepsOrPacks.push(id.toLowerCase()); + const installed = installExtensionResultsMap.get(`${id.toLowerCase()}-${profileLocation.toString()}`); + if (installed?.local) { + allDepsOrPacks = getAllDepsAndPacks(installed.local, profileLocation, allDepsOrPacks); + } + } + return allDepsOrPacks; + }; + const getErrorResult = (task: IInstallExtensionTask) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, error }); + + const rollbackTasks: IUninstallExtensionTask[] = []; + for (const [key, { task, root }] of installingExtensionsMap) { + const result = installExtensionResultsMap.get(key); + if (!result) { + task.cancel(); + installExtensionResultsMap.set(key, getErrorResult(task)); + } + // If the extension is installed by a root task and the root task is failed, then uninstall the extension + else if (result.local && root && !installExtensionResultsMap.get(`${root.identifier.id.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local) { + rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation })); + installExtensionResultsMap.set(key, getErrorResult(task)); + } + } + for (const [key, { task }] of installingExtensionsMap) { + const result = installExtensionResultsMap.get(key); + if (!result?.local) { + continue; + } + if (task.options.donotIncludePackAndDependencies) { + continue; + } + const depsOrPacks = getAllDepsAndPacks(result.local, task.options.profileLocation, [result.local.identifier.id.toLowerCase()]).slice(1); + if (depsOrPacks.some(depOrPack => installingExtensionsMap.has(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`) && !installExtensionResultsMap.get(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local)) { + rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation })); + installExtensionResultsMap.set(key, getErrorResult(task)); + } + } + + if (rollbackTasks.length) { + await Promise.allSettled(rollbackTasks.map(async rollbackTask => { try { - await this.createUninstallExtensionTask(local, { versionOnly: true, profileLocation }).run(); - this.logService.info('Rollback: Uninstalled extension', local.identifier.id); + await rollbackTask.run(); + this.logService.info('Rollback: Uninstalled extension', rollbackTask.extension.identifier.id); } catch (error) { - this.logService.warn('Rollback: Error while uninstalling extension', local.identifier.id, getErrorMessage(error)); + this.logService.warn('Rollback: Error while uninstalling extension', rollbackTask.extension.identifier.id, getErrorMessage(error)); } })); } - // cancel all tasks and collect error results - for (const { task } of installingExtensionsMap.values()) { - task.cancel(); - results.push({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.profileLocation, error }); - } - throw error; } finally { // Finally, remove all the tasks from the cache for (const { task } of installingExtensionsMap.values()) { if (task.source && !URI.isUri(task.source)) { - this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.profileLocation)); + this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.options.profileLocation)); } } - if (results.length) { + if (installExtensionResultsMap.size) { + const results = [...installExtensionResultsMap.values()]; + for (const result of results) { + if (result.local) { + this.logService.info(`Extension installed successfully:`, result.identifier.id, result.profileLocation.toString()); + } + } this._onDidInstallExtensions.fire(results); } } @@ -392,20 +457,35 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return true; } - private async joinAllSettled(promises: Promise[]): Promise { + private async joinAllSettled(promises: Promise[], errorCode?: ExtensionManagementErrorCode): Promise { const results: T[] = []; - const errors: any[] = []; + const errors: ExtensionManagementError[] = []; const promiseResults = await Promise.allSettled(promises); for (const r of promiseResults) { if (r.status === 'fulfilled') { results.push(r.value); } else { - errors.push(r.reason); + errors.push(toExtensionManagementError(r.reason, errorCode)); } } - // If there are errors, throw the error. - if (errors.length) { throw joinErrors(errors); } - return results; + + if (!errors.length) { + return results; + } + + // Throw if there are errors + if (errors.length === 1) { + throw errors[0]; + } + + let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); + for (const current of errors) { + error = new ExtensionManagementError( + error.message ? `${error.message}, ${current.message}` : current.message, + current.code !== ExtensionManagementErrorCode.Unknown && current.code !== ExtensionManagementErrorCode.Internal ? current.code : error.code + ); + } + throw error; } private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { @@ -490,6 +570,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion); if (!compatibleExtension) { + const incompatibleApiProposalsMessages: string[] = []; + if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) { + throw new ExtensionManagementError(nls.localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi); + } /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.displayName ?? extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); @@ -534,43 +618,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return compatibleExtension; } - private async uninstallExtension(extension: ILocalExtension, options: UninstallOptions): Promise { - const uninstallOptions: UninstallExtensionTaskOptions = { - ...options, - profileLocation: extension.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation() - }; - const getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${uninstallOptions.versionOnly ? `-${extension.manifest.version}` : ''}${uninstallOptions.profileLocation ? `@${uninstallOptions.profileLocation.toString()}` : ''}`; - const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension.identifier)); - if (uninstallExtensionTask) { - this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); - return uninstallExtensionTask.waitUntilTaskIsFinished(); - } + async uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise { - const createUninstallExtensionTask = (extension: ILocalExtension): IUninstallExtensionTask => { + const getUninstallExtensionTaskKey = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions) => `${extension.identifier.id.toLowerCase()}${uninstallOptions.versionOnly ? `-${extension.manifest.version}` : ''}@${uninstallOptions.profileLocation.toString()}`; + + const createUninstallExtensionTask = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions): IUninstallExtensionTask => { const uninstallExtensionTask = this.createUninstallExtensionTask(extension, uninstallOptions); - this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension.identifier), uninstallExtensionTask); - if (uninstallOptions.profileLocation) { - this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); - } else { - this.logService.info('Uninstalling extension:', `${extension.identifier.id}@${extension.manifest.version}`); - } + this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension, uninstallOptions), uninstallExtensionTask); + this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); return uninstallExtensionTask; }; - const postUninstallExtension = (extension: ILocalExtension, error?: ExtensionManagementError): void => { + const postUninstallExtension = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions, error?: ExtensionManagementError): void => { if (error) { - if (uninstallOptions.profileLocation) { - this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message); - } else { - this.logService.error('Failed to uninstall extension:', `${extension.identifier.id}@${extension.manifest.version}`, error.message); - } + this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message); } else { - if (uninstallOptions.profileLocation) { - this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); - } else { - this.logService.info('Successfully uninstalled extension:', `${extension.identifier.id}@${extension.manifest.version}`); - } + this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); } reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error }); this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); @@ -578,64 +642,91 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const allTasks: IUninstallExtensionTask[] = []; const processedTasks: IUninstallExtensionTask[] = []; + const alreadyRequestedUninstalls: Promise[] = []; + + const installedExtensionsMap = new ResourceMap(); + + for (const { extension, options } of extensions) { + const uninstallOptions: UninstallExtensionTaskOptions = { + ...options, + profileLocation: extension.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options?.profileLocation ?? this.getCurrentExtensionsManifestLocation() + }; + const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension, uninstallOptions)); + if (uninstallExtensionTask) { + this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); + alreadyRequestedUninstalls.push(uninstallExtensionTask.waitUntilTaskIsFinished()); + } else { + allTasks.push(createUninstallExtensionTask(extension, uninstallOptions)); + } + } try { - allTasks.push(createUninstallExtensionTask(extension)); - const installed = await this.getInstalled(ExtensionType.User, uninstallOptions.profileLocation); - if (uninstallOptions.donotIncludePack) { - this.logService.info('Uninstalling the extension without including packed extension', `${extension.identifier.id}@${extension.manifest.version}`); - } else { - const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); - for (const packedExtension of packedExtensions) { - if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension.identifier))) { - this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id); - } else { - allTasks.push(createUninstallExtensionTask(packedExtension)); + for (const task of allTasks.slice(0)) { + let installed = installedExtensionsMap.get(task.options.profileLocation); + if (!installed) { + installedExtensionsMap.set(task.options.profileLocation, installed = await this.getInstalled(ExtensionType.User, task.options.profileLocation)); + } + + if (task.options.donotIncludePack) { + this.logService.info('Uninstalling the extension without including packed extension', `${task.extension.identifier.id}@${task.extension.manifest.version}`); + } else { + const packedExtensions = this.getAllPackExtensionsToUninstall(task.extension, installed); + for (const packedExtension of packedExtensions) { + if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension, task.options))) { + this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id); + } else { + allTasks.push(createUninstallExtensionTask(packedExtension, task.options)); + } } } - } - - if (uninstallOptions.donotCheckDependents) { - this.logService.info('Uninstalling the extension without checking dependents', `${extension.identifier.id}@${extension.manifest.version}`); - } else { - this.checkForDependents(allTasks.map(task => task.extension), installed, extension); + if (task.options.donotCheckDependents) { + this.logService.info('Uninstalling the extension without checking dependents', `${task.extension.identifier.id}@${task.extension.manifest.version}`); + } else { + this.checkForDependents(allTasks.map(task => task.extension), installed, task.extension); + } } // Uninstall extensions in parallel and wait until all extensions are uninstalled / failed await this.joinAllSettled(allTasks.map(async task => { try { await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, uninstallOptions, CancellationToken.None))); + await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, task.options, CancellationToken.None))); // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. if (task.extension.identifier.uuid) { try { await this.galleryService.reportStatistic(task.extension.manifest.publisher, task.extension.manifest.name, task.extension.manifest.version, StatisticType.Uninstall); } catch (error) { /* ignore */ } } - postUninstallExtension(task.extension); } catch (e) { - const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ExtensionManagementErrorCode.Internal); - postUninstallExtension(task.extension, error); + const error = toExtensionManagementError(e); + postUninstallExtension(task.extension, task.options, error); throw error; } finally { processedTasks.push(task); } })); + if (alreadyRequestedUninstalls.length) { + await this.joinAllSettled(alreadyRequestedUninstalls); + } + + for (const task of allTasks) { + postUninstallExtension(task.extension, task.options); + } } catch (e) { - const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ExtensionManagementErrorCode.Internal); + const error = toExtensionManagementError(e); for (const task of allTasks) { // cancel the tasks try { task.cancel(); } catch (error) { /* ignore */ } if (!processedTasks.includes(task)) { - postUninstallExtension(task.extension, error); + postUninstallExtension(task.extension, task.options, error); } } throw error; } finally { // Remove tasks from cache for (const task of allTasks) { - if (!this.uninstallingExtensions.delete(getUninstallExtensionTaskKey(task.extension.identifier))) { + if (!this.uninstallingExtensions.delete(getUninstallExtensionTaskKey(task.extension, task.options))) { this.logService.warn('Uninstallation task is not found in the cache', task.extension.identifier.id); } } @@ -716,7 +807,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract getTargetPlatform(): Promise; abstract zip(extension: ILocalExtension): Promise; - abstract unzip(zipLocation: URI): Promise; abstract getManifest(vsix: URI): Promise; abstract install(vsix: URI, options?: InstallOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; @@ -727,7 +817,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract reinstallFromGallery(extension: ILocalExtension): Promise; abstract cleanUp(): Promise; - abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; + abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; protected abstract getCurrentExtensionsManifestLocation(): URI; protected abstract createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask; @@ -735,31 +825,37 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected abstract copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial): Promise; } -export function joinErrors(errorOrErrors: (Error | string) | (Array)): Error { - const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; - if (errors.length === 1) { - return errors[0] instanceof Error ? errors[0] : new Error(errors[0]); - } - return errors.reduce((previousValue: Error, currentValue: Error | string) => { - return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); - }, new Error('')); -} - -export function toExtensionManagementError(error: Error): ExtensionManagementError { +export function toExtensionManagementError(error: Error, code?: ExtensionManagementErrorCode): ExtensionManagementError { if (error instanceof ExtensionManagementError) { return error; } + let extensionManagementError: ExtensionManagementError; if (error instanceof ExtensionGalleryError) { - const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Gallery); - e.stack = error.stack; - return e; + extensionManagementError = new ExtensionManagementError(error.message, error.code === ExtensionGalleryErrorCode.DownloadFailedWriting ? ExtensionManagementErrorCode.DownloadFailedWriting : ExtensionManagementErrorCode.Gallery); + } else { + extensionManagementError = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : (code ?? ExtensionManagementErrorCode.Internal)); } - const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Internal); - e.stack = error.stack; - return e; + extensionManagementError.stack = error.stack; + return extensionManagementError; } -function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, verificationStatus, duration, error, durationSinceUpdate }: { extensionData: any; verificationStatus?: ExtensionVerificationStatus; duration?: number; durationSinceUpdate?: number; error?: Error }): void { +function reportTelemetry(telemetryService: ITelemetryService, eventName: string, + { + extensionData, + verificationStatus, + duration, + error, + source, + durationSinceUpdate + }: { + extensionData: any; + verificationStatus?: + ExtensionVerificationStatus; + duration?: number; + durationSinceUpdate?: number; + source?: string; + error?: ExtensionManagementError | ExtensionGalleryError; + }): void { let errorcode: string | undefined; let errorcodeDetail: string | undefined; @@ -776,13 +872,9 @@ function reportTelemetry(telemetryService: ITelemetryService, eventName: string, } if (error) { - if (error instanceof ExtensionManagementError || error instanceof ExtensionGalleryError) { - errorcode = error.code; - if (error.code === ExtensionManagementErrorCode.Signature) { - errorcodeDetail = error.message; - } - } else { - errorcode = ExtensionManagementErrorCode.Internal; + errorcode = error.code; + if (error.code === ExtensionManagementErrorCode.Signature) { + errorcodeDetail = error.message; } } @@ -796,6 +888,7 @@ function reportTelemetry(telemetryService: ITelemetryService, eventName: string, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] @@ -820,12 +913,13 @@ function reportTelemetry(telemetryService: ITelemetryService, eventName: string, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ - telemetryService.publicLog(eventName, { ...extensionData, verificationStatus, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate }); + telemetryService.publicLog(eventName, { ...extensionData, verificationStatus, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate, source }); } export abstract class AbstractExtensionTask { @@ -838,7 +932,7 @@ export abstract class AbstractExtensionTask { return this.cancellablePromise!; } - async run(): Promise { + run(): Promise { if (!this.cancellablePromise) { this.cancellablePromise = createCancelablePromise(token => this.doRun(token)); } diff --git a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 74c69f11e5a..61c8816e3aa 100644 --- a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts @@ -119,7 +119,7 @@ export class StorageManager extends Disposable { } set(key: string, value: IExtensionIdentifier[], scope: StorageScope): void { - const newValue: string = JSON.stringify(value.map(({ id, uuid }) => ({ id, uuid }))); + const newValue: string = JSON.stringify(value.map(({ id, uuid }): IExtensionIdentifier => ({ id, uuid }))); const oldValue = this._get(key, scope); if (oldValue !== newValue) { if (scope === StorageScope.PROFILE) { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 0c9ec9e98e8..bbe96046957 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -12,13 +12,13 @@ import { isWeb, platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; +import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; +import { areApiProposalsCompatible, isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -209,10 +209,12 @@ const PropertyType = { ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine', PreRelease: 'Microsoft.VisualStudio.Code.PreRelease', + EnabledApiProposals: 'Microsoft.VisualStudio.Code.EnabledApiProposals', LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages', WebExtension: 'Microsoft.VisualStudio.Code.WebExtension', SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink', SupportLink: 'Microsoft.VisualStudio.Services.Links.Support', + ExecutesCode: 'Microsoft.VisualStudio.Code.ExecutesCode', }; interface ICriterium { @@ -430,6 +432,17 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean { return values.length > 0 && values[0].value === 'true'; } +function executesCode(version: IRawGalleryExtensionVersion): boolean | undefined { + const values = version.properties ? version.properties.filter(p => p.key === PropertyType.ExecutesCode) : []; + return values.length > 0 ? values[0].value === 'true' : undefined; +} + +function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] { + const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : []; + const value = (values.length > 0 && values[0].value) || ''; + return value ? value.split(',') : []; +} + function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] { const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : []; const value = (values.length > 0 && values[0].value) || ''; @@ -508,7 +521,7 @@ function setTelemetry(extension: IGalleryExtension, index: number, querySource?: function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], queryContext?: IStringDictionary): IGalleryExtension { const latestVersion = galleryExtension.versions[0]; - const assets = { + const assets: IGalleryExtensionAssets = { manifest: getVersionAsset(version, AssetType.Manifest), readme: getVersionAsset(version, AssetType.Details), changelog: getVersionAsset(version, AssetType.Changelog), @@ -521,6 +534,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller }; return { + type: 'gallery', identifier: { id: getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName), uuid: galleryExtension.extensionId @@ -547,9 +561,11 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller dependencies: getExtensions(version, PropertyType.Dependency), extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version), + enabledApiProposals: getEnabledApiProposals(version), localizedLanguages: getLocalizedLanguages(version), targetPlatform: getTargetPlatformForExtensionVersion(version), - isPreReleaseVersion: isPreReleaseVersion(version) + isPreReleaseVersion: isPreReleaseVersion(version), + executesCode: executesCode(version) }, hasPreReleaseVersion: isPreReleaseVersion(latestVersion), hasReleaseVersion: true, @@ -578,6 +594,7 @@ interface IRawExtensionsControlManifest { additionalInfo?: string; }>; search?: ISearchPrefferedResults[]; + extensionsEnabledWithPreRelease?: string[]; } abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { @@ -588,7 +605,8 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi private readonly extensionsGallerySearchUrl: string | undefined; private readonly extensionsControlUrl: string | undefined; - private readonly commonHeadersPromise: Promise>; + private readonly commonHeadersPromise: Promise; + private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( storageService: IStorageService | undefined, @@ -605,6 +623,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.extensionsGalleryUrl = isPPEEnabled ? config.servicePPEUrl : config?.serviceUrl; this.extensionsGallerySearchUrl = isPPEEnabled ? undefined : config?.searchUrl; this.extensionsControlUrl = config?.controlUrl; + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; this.commonHeadersPromise = resolveMarketplaceHeaders( productService.version, productService, @@ -703,7 +722,26 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } engine = manifest.engines.vscode; } - return isEngineValid(engine, productVersion.version, productVersion.date); + + if (!isEngineValid(engine, productVersion.version, productVersion.date)) { + return false; + } + + if (!this.areApiProposalsCompatible(extension.identifier, extension.properties.enabledApiProposals)) { + return false; + } + + return true; + } + + private areApiProposalsCompatible(extensionIdentifier: IExtensionIdentifier, enabledApiProposals: string[] | undefined): boolean { + if (!enabledApiProposals) { + return true; + } + if (!this.extensionsEnabledWithApiProposalVersion.includes(extensionIdentifier.id.toLowerCase())) { + return true; + } + return areApiProposalsCompatible(enabledApiProposals); } private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { @@ -798,7 +836,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return extensions; }; - return { firstPage: extensions, total, pageSize: query.pageSize, getPage } as IPager; + return { firstPage: extensions, total, pageSize: query.pageSize, getPage }; } private async queryGalleryExtensions(query: Query, criteria: IExtensionCriteria, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> { @@ -914,7 +952,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { + if (await this.isValidVersion( + extensionIdentifier.id, + rawGalleryExtensionVersion, + includePreRelease ? 'any' : 'release', + criteria.compatible, + allTargetPlatforms, + criteria.targetPlatform, + criteria.productVersion) + ) { + if (criteria.compatible && !this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(rawGalleryExtensionVersion))) { + return null; + } return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { @@ -956,7 +1005,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi }; const stopWatch = new StopWatch(); - let context: IRequestContext | undefined, error: ExtensionGalleryError | undefined, total: number = 0; + let context: IRequestContext | undefined, errorCode: ExtensionGalleryErrorCode | undefined, total: number = 0; try { context = await this.requestService.request({ @@ -980,17 +1029,26 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { galleryExtensions, total, - context: { + context: context.res.headers['activityid'] ? { [ACTIVITY_HEADER_NAME]: context.res.headers['activityid'] - } + } : {} }; } return { galleryExtensions: [], total }; } catch (e) { - const errorCode = isCancellationError(e) ? ExtensionGalleryErrorCode.Cancelled : getErrorMessage(e).startsWith('XHR timeout') ? ExtensionGalleryErrorCode.Timeout : ExtensionGalleryErrorCode.Failed; - error = new ExtensionGalleryError(getErrorMessage(e), errorCode); - throw error; + if (isCancellationError(e)) { + errorCode = ExtensionGalleryErrorCode.Cancelled; + throw e; + } else { + const errorMessage = getErrorMessage(e); + errorCode = isOfflineError(e) + ? ExtensionGalleryErrorCode.Offline + : errorMessage.startsWith('XHR timeout') + ? ExtensionGalleryErrorCode.Timeout + : ExtensionGalleryErrorCode.Failed; + throw new ExtensionGalleryError(errorMessage, errorCode); + } } finally { this.telemetryService.publicLog2('galleryService:query', { ...query.telemetryData, @@ -999,7 +1057,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi success: !!context && isSuccess(context), responseBodySize: context?.res.headers['Content-Length'], statusCode: context ? String(context.res.statusCode) : undefined, - errorCode: error?.code, + errorCode, count: String(total) }); } @@ -1028,16 +1086,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#download', extension.identifier.id); const data = getGalleryExtensionTelemetryData(extension); const startTime = new Date().getTime(); - /* __GDPR__ - "galleryService:downloadVSIX" : { - "owner": "sandy081", - "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration }); const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; const downloadAsset = operationParam ? { @@ -1047,8 +1095,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); - await this.fileService.writeFile(location, context.stream); - log(new Date().getTime() - startTime); + + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + + /* __GDPR__ + "galleryService:downloadVSIX" : { + "owner": "sandy081", + "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration: new Date().getTime() - startTime }); } async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise { @@ -1059,7 +1128,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature); - await this.fileService.writeFile(location, context.stream); + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + } async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { @@ -1109,15 +1189,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return ''; } - async getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { let query = new Query() .withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, 1); - if (extension.identifier.uuid) { - query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid); + if (extensionIdentifier.uuid) { + query = query.withFilter(FilterType.ExtensionId, extensionIdentifier.uuid); } else { - query = query.withFilter(FilterType.ExtensionName, extension.identifier.id); + query = query.withFilter(FilterType.ExtensionName, extensionIdentifier.id); } const { galleryExtensions } = await this.queryRawGalleryExtensions(query, CancellationToken.None); @@ -1133,7 +1213,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const validVersions: IRawGalleryExtensionVersion[] = []; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { - if (await this.isValidVersion(extension.identifier.id, version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) { + if ( + (await this.isValidVersion( + extensionIdentifier.id, + version, includePreRelease ? 'any' : 'release', + true, + allTargetPlatforms, + targetPlatform)) + && this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(version)) + ) { validVersions.push(version); } } catch (error) { /* Ignore error and skip version */ } @@ -1234,6 +1322,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const malicious: IExtensionIdentifier[] = []; const deprecated: IStringDictionary = {}; const search: ISearchPrefferedResults[] = []; + const extensionsEnabledWithPreRelease: string[] = []; if (result) { for (const id of result.malicious) { malicious.push({ id }); @@ -1265,9 +1354,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi search.push(s); } } + if (Array.isArray(result.extensionsEnabledWithPreRelease)) { + for (const id of result.extensionsEnabledWithPreRelease) { + extensionsEnabledWithPreRelease.push(id.toLowerCase()); + } + } } - return { malicious, deprecated, search }; + return { malicious, deprecated, search, extensionsEnabledWithPreRelease }; } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 9dae82eba07..83ed0c519ca 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -17,8 +17,14 @@ export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); export const WEB_EXTENSION_TAG = '__web_extension'; export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; -export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; +export const EXTENSION_INSTALL_SOURCE_CONTEXT = 'extensionInstallSource'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export const EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT = 'clientTargetPlatform'; + +export const enum ExtensionInstallSource { + COMMAND = 'command', + SETTINGS_SYNC = 'settingsSync', +} export interface IProductVersion { readonly version: string; @@ -153,9 +159,11 @@ export interface IGalleryExtensionProperties { dependencies?: string[]; extensionPack?: string[]; engine?: string; + enabledApiProposals?: string[]; localizedLanguages?: string[]; targetPlatform: TargetPlatform; isPreReleaseVersion: boolean; + executesCode?: boolean; } export interface IGalleryExtensionAsset { @@ -198,6 +206,7 @@ export interface IGalleryExtensionVersion { } export interface IGalleryExtension { + type: 'gallery'; name: string; identifier: IGalleryExtensionIdentifier; version: string; @@ -255,7 +264,6 @@ export interface ILocalExtension extends IExtension { isMachineScoped: boolean; isApplicationScoped: boolean; publisherId: string | null; - publisherDisplayName: string | null; installedTimestamp?: number; isPreReleaseVersion: boolean; hasPreReleaseVersion: boolean; @@ -320,6 +328,7 @@ export interface IExtensionsControlManifest { readonly malicious: IExtensionIdentifier[]; readonly deprecated: IStringDictionary; readonly search: ISearchPrefferedResults[]; + readonly extensionsEnabledWithPreRelease?: string[]; } export const enum InstallOperation { @@ -361,7 +370,7 @@ export interface IExtensionGalleryService { getExtensions(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, token: CancellationToken): Promise; isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; - getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; + getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise; @@ -375,7 +384,7 @@ export interface IExtensionGalleryService { export interface InstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly source: URI | IGalleryExtension; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } @@ -387,14 +396,14 @@ export interface InstallExtensionResult { readonly local?: ILocalExtension; readonly error?: Error; readonly context?: IStringDictionary; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } export interface UninstallExtensionEvent { readonly identifier: IExtensionIdentifier; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } @@ -402,56 +411,67 @@ export interface UninstallExtensionEvent { export interface DidUninstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly error?: string; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } -export enum ExtensionManagementErrorCode { - Unsupported = 'Unsupported', - Deprecated = 'Deprecated', - Malicious = 'Malicious', - Incompatible = 'Incompatible', - IncompatibleTargetPlatform = 'IncompatibleTargetPlatform', - ReleaseVersionNotFound = 'ReleaseVersionNotFound', - Invalid = 'Invalid', - Download = 'Download', - DownloadSignature = 'DownloadSignature', - UpdateMetadata = 'UpdateMetadata', - Extract = 'Extract', - Scanning = 'Scanning', - Delete = 'Delete', - Rename = 'Rename', - CorruptZip = 'CorruptZip', - IncompleteZip = 'IncompleteZip', - Signature = 'Signature', - NotAllowed = 'NotAllowed', - Gallery = 'Gallery', - Unknown = 'Unknown', - Internal = 'Internal', +export interface DidUpdateExtensionMetadata { + readonly profileLocation: URI; + readonly local: ILocalExtension; } -export enum ExtensionSignaturetErrorCode { - UnknownError = 'UnknownError', - PackageIsInvalidZip = 'PackageIsInvalidZip', - SignatureArchiveIsInvalidZip = 'SignatureArchiveIsInvalidZip', +export const enum ExtensionGalleryErrorCode { + Timeout = 'Timeout', + Cancelled = 'Cancelled', + Failed = 'Failed', + DownloadFailedWriting = 'DownloadFailedWriting', + Offline = 'Offline', } -export class ExtensionManagementError extends Error { - constructor(message: string, readonly code: ExtensionManagementErrorCode) { +export class ExtensionGalleryError extends Error { + constructor(message: string, readonly code: ExtensionGalleryErrorCode) { super(message); this.name = code; } } -export enum ExtensionGalleryErrorCode { - Timeout = 'Timeout', +export const enum ExtensionManagementErrorCode { + Unsupported = 'Unsupported', + Deprecated = 'Deprecated', + Malicious = 'Malicious', + Incompatible = 'Incompatible', + IncompatibleApi = 'IncompatibleApi', + IncompatibleTargetPlatform = 'IncompatibleTargetPlatform', + ReleaseVersionNotFound = 'ReleaseVersionNotFound', + Invalid = 'Invalid', + Download = 'Download', + DownloadSignature = 'DownloadSignature', + DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, + UpdateMetadata = 'UpdateMetadata', + Extract = 'Extract', + Scanning = 'Scanning', + ScanningExtension = 'ScanningExtension', + ReadUninstalled = 'ReadUninstalled', + UnsetUninstalled = 'UnsetUninstalled', + Delete = 'Delete', + Rename = 'Rename', + IntializeDefaultProfile = 'IntializeDefaultProfile', + AddToProfile = 'AddToProfile', + InstalledExtensionNotFound = 'InstalledExtensionNotFound', + PostInstall = 'PostInstall', + CorruptZip = 'CorruptZip', + IncompleteZip = 'IncompleteZip', + Signature = 'Signature', + NotAllowed = 'NotAllowed', + Gallery = 'Gallery', Cancelled = 'Cancelled', - Failed = 'Failed' + Unknown = 'Unknown', + Internal = 'Internal', } -export class ExtensionGalleryError extends Error { - constructor(message: string, readonly code: ExtensionGalleryErrorCode) { +export class ExtensionManagementError extends Error { + constructor(message: string, readonly code: ExtensionManagementErrorCode) { super(message); this.name = code; } @@ -472,12 +492,20 @@ export type InstallOptions = { profileLocation?: URI; installOnlyNewlyAddedFromExtensionPack?: boolean; productVersion?: IProductVersion; + keepExisting?: boolean; /** * Context passed through to InstallExtensionResult */ context?: IStringDictionary; }; -export type UninstallOptions = { readonly donotIncludePack?: boolean; readonly donotCheckDependents?: boolean; readonly versionOnly?: boolean; readonly remove?: boolean; readonly profileLocation?: URI }; + +export type UninstallOptions = { + readonly profileLocation?: URI; + readonly donotIncludePack?: boolean; + readonly donotCheckDependents?: boolean; + readonly versionOnly?: boolean; + readonly remove?: boolean; +}; export interface IExtensionManagementParticipant { postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions, token: CancellationToken): Promise; @@ -485,6 +513,7 @@ export interface IExtensionManagementParticipant { } export type InstallExtensionInfo = { readonly extension: IGalleryExtension; readonly options: InstallOptions }; +export type UninstallExtensionInfo = { readonly extension: ILocalExtension; readonly options?: UninstallOptions }; export const IExtensionManagementService = createDecorator('extensionManagementService'); export interface IExtensionManagementService { @@ -494,10 +523,9 @@ export interface IExtensionManagementService { onDidInstallExtensions: Event; onUninstallExtension: Event; onDidUninstallExtension: Event; - onDidUpdateExtensionMetadata: Event; + onDidUpdateExtensionMetadata: Event; zip(extension: ILocalExtension): Promise; - unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; install(vsix: URI, options?: InstallOptions): Promise; canInstall(extension: IGalleryExtension): Promise; @@ -506,12 +534,14 @@ export interface IExtensionManagementService { installFromLocation(location: URI, profileLocation: URI): Promise; installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; + uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise; toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; - updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; + updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; + resetPinnedStateForAllUserExtensions(pinned: boolean): Promise; download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts index 2dd973658b9..064420130d1 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts @@ -100,7 +100,7 @@ export class ExtensionManagementCLI { } } - const installed = await this.extensionManagementService.getInstalled(ExtensionType.User, installOptions.profileLocation); + const installed = await this.extensionManagementService.getInstalled(undefined, installOptions.profileLocation); if (installVSIXInfos.length) { await Promise.all(installVSIXInfos.map(async ({ vsix, installOptions }) => { @@ -146,7 +146,7 @@ export class ExtensionManagementCLI { if (areSameExtensions(oldVersion.identifier, newVersion.identifier) && gt(newVersion.version, oldVersion.manifest.version)) { extensionsToUpdate.push({ extension: newVersion, - options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.isPreReleaseVersion } + options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation, isApplicationScoped: oldVersion.isApplicationScoped } }); } } @@ -224,7 +224,7 @@ export class ExtensionManagementCLI { } extensionsToInstall.push({ extension: gallery, - options: { ...installOptions, installGivenVersion: !!version }, + options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installOptions.isApplicationScoped || installedExtension?.isApplicationScoped }, }); })); @@ -253,7 +253,7 @@ export class ExtensionManagementCLI { const valid = await this.validateVSIX(manifest, force, installOptions.profileLocation, installedExtensions); if (valid) { try { - await this.extensionManagementService.install(vsix, installOptions); + await this.extensionManagementService.install(vsix, { ...installOptions, installGivenVersion: true }); this.logger.info(localize('successVsixInstall', "Extension '{0}' was successfully installed.", basename(vsix))); } catch (error) { if (isCancellationError(error)) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 6c3e289db7d..2785a41f718 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata, UninstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -43,7 +43,7 @@ export class ExtensionManagementChannel implements IServerChannel { onDidInstallExtensions: Event; onUninstallExtension: Event; onDidUninstallExtension: Event; - onDidUpdateExtensionMetadata: Event; + onDidUpdateExtensionMetadata: Event; constructor(private service: IExtensionManagementService, private getUriTransformer: (requestContext: any) => IURITransformer | null) { this.onInstallExtension = Event.buffer(service.onInstallExtension, true); @@ -89,7 +89,12 @@ export class ExtensionManagementChannel implements IServerChannel { }); } case 'onDidUpdateExtensionMetadata': { - return Event.map(this.onDidUpdateExtensionMetadata, e => transformOutgoingExtension(e, uriTransformer)); + return Event.map(this.onDidUpdateExtensionMetadata, e => { + return { + local: transformOutgoingExtension(e.local, uriTransformer), + profileLocation: transformOutgoingURI(e.profileLocation, uriTransformer) + }; + }); } } @@ -104,9 +109,6 @@ export class ExtensionManagementChannel implements IServerChannel { const uri = await this.service.zip(extension); return transformOutgoingURI(uri, uriTransformer); } - case 'unzip': { - return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); - } case 'install': { return this.service.install(transformIncomingURI(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); } @@ -135,6 +137,10 @@ export class ExtensionManagementChannel implements IServerChannel { case 'uninstall': { return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); } + case 'uninstallExtensions': { + const arg: UninstallExtensionInfo[] = args[0]; + return this.service.uninstallExtensions(arg.map(({ extension, options }) => ({ extension: transformIncomingExtension(extension, uriTransformer), options: transformIncomingOptions(options, uriTransformer) }))); + } case 'reinstallFromGallery': { return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); } @@ -153,6 +159,9 @@ export class ExtensionManagementChannel implements IServerChannel { const e = await this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1], transformIncomingURI(args[2], uriTransformer)); return transformOutgoingExtension(e, uriTransformer); } + case 'resetPinnedStateForAllUserExtensions': { + return this.service.resetPinnedStateForAllUserExtensions(args[0]); + } case 'getExtensionsControlManifest': { return this.service.getExtensionsControlManifest(); } @@ -168,7 +177,11 @@ export class ExtensionManagementChannel implements IServerChannel { } } -export type ExtensionEventResult = InstallExtensionEvent | InstallExtensionResult | UninstallExtensionEvent | DidUninstallExtensionEvent; +export interface ExtensionEventResult { + readonly profileLocation: URI; + readonly local?: ILocalExtension; + readonly applicationScoped?: boolean; +} export class ExtensionManagementChannelClient extends Disposable implements IExtensionManagementService { @@ -186,7 +199,7 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt private readonly _onDidUninstallExtension = this._register(new Emitter()); get onDidUninstallExtension() { return this._onDidUninstallExtension.event; } - private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); + private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; } constructor(private readonly channel: IChannel) { @@ -195,16 +208,12 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt this._register(this.channel.listen('onDidInstallExtensions')(results => this.fireEvent(this._onDidInstallExtensions, results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))))); this._register(this.channel.listen('onUninstallExtension')(e => this.fireEvent(this._onUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); this._register(this.channel.listen('onDidUninstallExtension')(e => this.fireEvent(this._onDidUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); - this._register(this.channel.listen('onDidUpdateExtensionMetadata')(e => this._onDidUpdateExtensionMetadata.fire(transformIncomingExtension(e, null)))); + this._register(this.channel.listen('onDidUpdateExtensionMetadata')(e => this.fireEvent(this._onDidUpdateExtensionMetadata, { profileLocation: URI.revive(e.profileLocation), local: transformIncomingExtension(e.local, null) }))); } - protected fireEvent(event: Emitter, data: InstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: InstallExtensionResult[]): void; - protected fireEvent(event: Emitter, data: UninstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: DidUninstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: ExtensionEventResult): void; - protected fireEvent(event: Emitter, data: ExtensionEventResult[]): void; - protected fireEvent(event: Emitter, data: E): void { + protected fireEvent(event: Emitter, data: E): void; + protected fireEvent(event: Emitter, data: E[]): void; + protected fireEvent(event: Emitter, data: E | E[]): void { event.fire(data); } @@ -233,10 +242,6 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); } - unzip(zipLocation: URI): Promise { - return Promise.resolve(this.channel.call('unzip', [zipLocation])); - } - install(vsix: URI, options?: InstallOptions): Promise { return Promise.resolve(this.channel.call('install', [vsix, options])).then(local => transformIncomingExtension(local, null)); } @@ -270,6 +275,14 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('uninstall', [extension, options])); } + uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise { + if (extensions.some(e => e.extension.isWorkspaceScoped)) { + throw new Error('Cannot uninstall a workspace extension'); + } + return Promise.resolve(this.channel.call('uninstallExtensions', [extensions])); + + } + reinstallFromGallery(extension: ILocalExtension): Promise { return Promise.resolve(this.channel.call('reinstallFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } @@ -284,6 +297,10 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt .then(extension => transformIncomingExtension(extension, null)); } + resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { + return this.channel.call('resetPinnedStateForAllUserExtensions', [pinned]); + } + toggleAppliationScope(local: ILocalExtension, fromProfileLocation: URI): Promise { return this.channel.call('toggleAppliationScope', [local, fromProfileLocation]) .then(extension => transformIncomingExtension(extension, null)); diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index e7e7bab19e6..301c7b18e70 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -42,7 +42,7 @@ export class ExtensionKey { readonly id: string; constructor( - identifier: IExtensionIdentifier, + readonly identifier: IExtensionIdentifier, readonly version: string, readonly targetPlatform: TargetPlatform = TargetPlatform.UNDEFINED, ) { @@ -120,6 +120,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any "GalleryExtensionTelemetryData" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "name": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "galleryId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -136,6 +137,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension): return { id: new TelemetryTrustedValue(extension.identifier.id), name: new TelemetryTrustedValue(extension.name), + version: extension.version, galleryId: extension.identifier.uuid, publisherId: extension.publisherId, publisherName: extension.publisher, diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index fddaf329af0..d3d1816b40b 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -306,7 +306,7 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable type ErrorClassification = { owner: 'sandy081'; comment: 'Information about the error that occurred while scanning'; - code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'error code' }; + code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; }; const error = new ExtensionsProfileScanningError(`Invalid extensions content in ${file.toString()}`, ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT); this.telemetryService.publicLogError2<{ code: string }, ErrorClassification>('extensionsProfileScanningError', { code: error.code }); diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 0b64c8b9b33..7d4c51d21bb 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -24,7 +24,7 @@ import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -46,6 +46,7 @@ interface IRelaxedScannedExtension { manifest: IRelaxedExtensionManifest; location: URI; targetPlatform: TargetPlatform; + publisherDisplayName?: string; metadata: Metadata | undefined; isValid: boolean; validations: readonly [Severity, string][]; @@ -553,14 +554,19 @@ type NlsConfiguration = { class ExtensionsScanner extends Disposable { + private readonly extensionsEnabledWithApiProposalVersion: string[]; + constructor( private readonly obsoleteFile: URI, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @IFileService protected readonly fileService: IFileService, + @IProductService productService: IProductService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @ILogService protected readonly logService: ILogService ) { super(); + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; } async scanExtensions(input: ExtensionScannerInput): Promise { @@ -652,18 +658,26 @@ class ExtensionsScanner extends Disposable { const type = metadata?.isSystem ? ExtensionType.System : input.type; const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); - const extension = { + let extension: IRelaxedScannedExtension = { type, identifier, manifest, location: input.location, isBuiltin, targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED, + publisherDisplayName: metadata?.publisherDisplayName, metadata, isValid: true, validations: [] }; - return input.validate ? this.validate(extension, input) : extension; + if (input.validate) { + extension = this.validate(extension, input); + } + if (manifest.enabledApiProposals && (!this.environmentService.isBuilt || this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase()))) { + manifest.originalEnabledApiProposals = manifest.enabledApiProposals; + manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } + return extension; } } catch (e) { if (input.type !== ExtensionType.System) { @@ -675,7 +689,8 @@ class ExtensionsScanner extends Disposable { validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { let isValid = true; - const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin); + const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); + const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, validateApiVersion); for (const [severity, message] of validations) { if (severity === Severity.Error) { isValid = false; @@ -687,7 +702,7 @@ class ExtensionsScanner extends Disposable { return extension; } - async scanExtensionManifest(extensionLocation: URI): Promise { + private async scanExtensionManifest(extensionLocation: URI): Promise { const manifestLocation = joinPath(extensionLocation, 'package.json'); let content; try { @@ -711,7 +726,7 @@ class ExtensionsScanner extends Disposable { return null; } if (getNodeType(manifest) !== 'object') { - this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", manifestLocation.path))); + this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path))); return null; } return manifest; @@ -876,9 +891,11 @@ class CachedExtensionsScanner extends ExtensionsScanner { @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IFileService fileService: IFileService, + @IProductService productService: IProductService, + @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService ) { - super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, logService); + super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); } override async scanExtensions(input: ExtensionScannerInput): Promise { @@ -886,7 +903,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { const cacheContents = await this.readExtensionCache(cacheFile); this.input = input; if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, this.input)) { - this.logService.debug('Using cached extensions scan result', input.location.toString()); + this.logService.debug('Using cached extensions scan result', input.type === ExtensionType.System ? 'system' : 'user', input.location.toString()); this.cacheValidatorThrottler.trigger(() => this.validateCache()); return cacheContents.result.map((extension) => { // revive URI object @@ -979,6 +996,7 @@ export function toExtensionDescription(extension: IScannedExtension, isUnderDeve extensionLocation: extension.location, uuid: extension.identifier.uuid, targetPlatform: extension.targetPlatform, + publisherDisplayName: extension.publisherDisplayName, ...extension.manifest, }; } diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 65cd11430ef..0ddae28ed93 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -13,15 +13,29 @@ import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { Promises as FSPromises } from 'vs/base/node/pfs'; -import { CorruptZipMessage } from 'vs/base/node/zip'; +import { buffer, CorruptZipMessage } from 'vs/base/node/zip'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionVerificationStatus } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; -import { ExtensionManagementError, ExtensionManagementErrorCode, ExtensionSignaturetErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionVerificationStatus, toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionSignatureVerificationError, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { fromExtractError } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; +import { ExtensionSignatureVerificationError, ExtensionSignatureVerificationCode, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +type RetryDownloadClassification = { + owner: 'sandy081'; + comment: 'Event reporting the retry of downloading'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; + attempts: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of Attempts' }; +}; +type RetryDownloadEvent = { + extensionId: string; + attempts: number; +}; export class ExtensionsDownloader extends Disposable { @@ -37,6 +51,7 @@ export class ExtensionsDownloader extends Disposable { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -45,31 +60,35 @@ export class ExtensionsDownloader extends Disposable { this.cleanUpPromise = this.cleanUp(); } - async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { + async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { await this.cleanUpPromise; - const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); - try { - await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); - } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download); - } + const location = await this.downloadVSIX(extension, operation); let verificationStatus: ExtensionVerificationStatus = false; if (verifySignature && this.shouldVerifySignature(extension)) { - const signatureArchiveLocation = await this.downloadSignatureArchive(extension); + + let signatureArchiveLocation; try { - verificationStatus = await this.extensionSignatureVerificationService.verify(location.fsPath, signatureArchiveLocation.fsPath, this.logService.getLevel() === LogLevel.Trace); + signatureArchiveLocation = await this.downloadSignatureArchive(extension); } catch (error) { - const sigError = error as ExtensionSignatureVerificationError; - verificationStatus = sigError.code; - if (sigError.output) { - this.logService.trace(`Extension signature verification details for ${extension.identifier.id} ${extension.version}:\n${sigError.output}`); + try { + // Delete the downloaded VSIX if signature archive download fails + await this.delete(location); + } catch (error) { + this.logService.error(error); } - if (verificationStatus === ExtensionSignaturetErrorCode.PackageIsInvalidZip || verificationStatus === ExtensionSignaturetErrorCode.SignatureArchiveIsInvalidZip) { + throw error; + } + + try { + verificationStatus = await this.extensionSignatureVerificationService.verify(extension, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); + } catch (error) { + verificationStatus = (error as ExtensionSignatureVerificationError).code; + if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { try { - // Delete the downloaded vsix before throwing the error + // Delete the downloaded vsix if VSIX or signature archive is invalid await this.delete(location); } catch (error) { this.logService.error(error); @@ -86,14 +105,6 @@ export class ExtensionsDownloader extends Disposable { } } - if (verificationStatus === true) { - this.logService.info(`Extension signature is verified: ${extension.identifier.id}`); - } else if (verificationStatus === false) { - this.logService.info(`Extension signature verification is not done: ${extension.identifier.id}`); - } else { - this.logService.warn(`Extension signature verification failed with error '${verificationStatus}': ${extension.identifier.id}`); - } - return { location, verificationStatus }; } @@ -107,16 +118,64 @@ export class ExtensionsDownloader extends Disposable { return isBoolean(value) ? value : true; } - private async downloadSignatureArchive(extension: IGalleryExtension): Promise { - await this.cleanUpPromise; - - const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`); + private async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise { try { - await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location)); - } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.DownloadSignature); + const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); + const attempts = await this.doDownload(extension, 'vsix', async () => { + await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); + try { + await this.validate(location.fsPath, 'extension/package.json'); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e)); + } + throw error; + } + }, 2); + + if (attempts > 1) { + this.telemetryService.publicLog2('extensiongallery:downloadvsix:retry', { + extensionId: extension.identifier.id, + attempts + }); + } + + return location; + } catch (e) { + throw toExtensionManagementError(e, ExtensionManagementErrorCode.Download); + } + } + + private async downloadSignatureArchive(extension: IGalleryExtension): Promise { + try { + const location = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); + const attempts = await this.doDownload(extension, 'sigzip', async () => { + await this.extensionGalleryService.downloadSignatureArchive(extension, location); + try { + await this.validate(location.fsPath, '.signature.p7s'); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e)); + } + throw error; + } + }, 2); + + if (attempts > 1) { + this.telemetryService.publicLog2('extensiongallery:downloadsigzip:retry', { + extensionId: extension.identifier.id, + attempts + }); + } + + return location; + } catch (e) { + throw toExtensionManagementError(e, ExtensionManagementErrorCode.DownloadSignature); } - return location; } private async downloadFile(extension: IGalleryExtension, location: URI, downloadFn: (location: URI) => Promise): Promise { @@ -133,18 +192,23 @@ export class ExtensionsDownloader extends Disposable { // Download to temporary location first only if file does not exist const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); - if (!await this.fileService.exists(tempLocation)) { + try { await downloadFn(tempLocation); + } catch (error) { + try { + await this.fileService.del(tempLocation); + } catch (e) { /* ignore */ } + throw error; } try { // Rename temp location to original await FSPromises.rename(tempLocation.fsPath, location.fsPath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - try { - await this.fileService.del(tempLocation); - } catch (e) { /* ignore */ } - if (error.code === 'ENOTEMPTY') { + try { await this.fileService.del(tempLocation); } catch (e) { /* ignore */ } + let exists = false; + try { exists = await this.fileService.exists(location); } catch (e) { /* ignore */ } + if (exists) { this.logService.info(`Rename failed because the file was downloaded by another source. So ignoring renaming.`, extension.identifier.id, location.path); } else { this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the file from downloaded location`, tempLocation.path); @@ -153,6 +217,29 @@ export class ExtensionsDownloader extends Disposable { } } + private async doDownload(extension: IGalleryExtension, name: string, downloadFn: () => Promise, retries: number): Promise { + let attempts = 1; + while (true) { + try { + await downloadFn(); + return attempts; + } catch (e) { + if (attempts++ > retries) { + throw e; + } + this.logService.warn(`Failed downloading ${name}. ${getErrorMessage(e)}. Retry again...`, extension.identifier.id); + } + } + } + + protected async validate(zipPath: string, filePath: string): Promise { + try { + await buffer(zipPath, filePath); + } catch (e) { + throw fromExtractError(e); + } + } + async delete(location: URI): Promise { await this.cleanUpPromise; await this.fileService.del(location); diff --git a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts index 8bcaf43a632..8de8416e712 100644 --- a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts +++ b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts @@ -116,19 +116,19 @@ export class ExtensionsLifecycle extends Disposable { const onStderr = Event.fromNodeEventEmitter(extensionUninstallProcess.stderr!, 'data'); // Log output - onStdout(data => this.logService.info(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data)); - onStderr(data => this.logService.error(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data)); + this._register(onStdout(data => this.logService.info(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data))); + this._register(onStderr(data => this.logService.error(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data))); const onOutput = Event.any( - Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] })), - Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] })) + Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] }), this._store), + Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }), this._store) ); // Debounce all output, so we can render it in the Chrome console as a group const onDebouncedOutput = Event.debounce(onOutput, (r, o) => { return r ? { data: r.data + o.data, format: [...r.format, ...o.format] } : { data: o.data, format: o.format }; - }, 100); + }, 100, undefined, undefined, undefined, this._store); // Print out output onDebouncedOutput(data => { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 56e32e7a269..75ab2baae27 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,40 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { Promises, Queue } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { getErrorMessage } from 'vs/base/common/errors'; +import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceSet } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; -import { isBoolean, isUndefined } from 'vs/base/common/types'; +import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; -import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip'; +import { extract, IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, Metadata, InstallOptions, - IProductVersion + IProductVersion, + EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT, } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; +import { fromExtractError, getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -49,12 +51,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -interface InstallableExtension { - zipPath: string; - key: ExtensionKey; - metadata: Metadata; -} - export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; @@ -63,6 +59,8 @@ export interface INativeServerExtensionManagementService extends IExtensionManag markAsUninstalled(...extensions: IExtension[]): Promise; } +type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionVerificationStatus }; + const DELETED_FOLDER_POSTFIX = '.vsctmp'; export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService { @@ -71,7 +69,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private readonly manifestCache: ExtensionsManifestCache; private readonly extensionsDownloader: ExtensionsDownloader; - private readonly installGalleryExtensionsTasks = new Map(); + private readonly extractingGalleryExtensions = new Map>(); constructor( @IExtensionGalleryService galleryService: IExtensionGalleryService, @@ -81,7 +79,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IDownloadService private downloadService: IDownloadService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, @IProductService productService: IProductService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -113,12 +111,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return URI.file(location); } - async unzip(zipLocation: URI): Promise { - this.logService.trace('ExtensionManagementService#unzip', zipLocation.toString()); - const local = await this.install(zipLocation); - return local.identifier; - } - async getManifest(vsix: URI): Promise { const { location, cleanup } = await this.downloadVsix(vsix); const zipPath = path.resolve(location.fsPath); @@ -189,7 +181,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return extensionsToInstall; } - async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { + async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise { this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id); if (metadata.isPreReleaseVersion) { metadata.preRelease = true; @@ -207,7 +199,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } local = await this.extensionsScanner.updateMetadata(local, metadata, profileLocation); this.manifestCache.invalidate(profileLocation); - this._onDidUpdateExtensionMetadata.fire(local); + this._onDidUpdateExtensionMetadata.fire({ local, profileLocation }); return local; } @@ -281,21 +273,87 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask { - if (URI.isUri(extension)) { - return new InstallVSIXTask(manifest, extension, options, this.galleryService, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService); - } - - const key = ExtensionKey.create(extension).toString(); - let installExtensionTask = this.installGalleryExtensionsTasks.get(key); - if (!installExtensionTask) { - this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService, this.telemetryService)); - installExtensionTask.waitUntilTaskIsFinished().finally(() => this.installGalleryExtensionsTasks.delete(key)); - } - return installExtensionTask; + const extensionKey = extension instanceof URI ? new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version) : ExtensionKey.create(extension); + return this.instantiationService.createInstance(InstallExtensionInProfileTask, extensionKey, manifest, extension, options, (operation, token) => { + if (extension instanceof URI) { + return this.extractVSIX(extensionKey, extension, options, token); + } + let promise = this.extractingGalleryExtensions.get(extensionKey.toString()); + if (!promise) { + this.extractingGalleryExtensions.set(extensionKey.toString(), promise = this.downloadAndExtractGalleryExtension(extensionKey, extension, operation, options, token)); + promise.finally(() => this.extractingGalleryExtensions.delete(extensionKey.toString())); + } + return promise; + }, this.extensionsScanner); } protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { - return new UninstallExtensionTask(extension, options.profileLocation, this.extensionsProfileScannerService); + return new UninstallExtensionInProfileTask(extension, options, this.extensionsProfileScannerService); + } + + private async downloadAndExtractGalleryExtension(extensionKey: ExtensionKey, gallery: IGalleryExtension, operation: InstallOperation, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { + const { verificationStatus, location } = await this.extensionsDownloader.download(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); + try { + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + // validate manifest + const manifest = await getManifest(location.fsPath); + if (!new ExtensionKey(gallery.identifier, gallery.version).equals(new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version))) { + throw new ExtensionManagementError(nls.localize('invalidManifest', "Cannot install '{0}' extension because of manifest mismatch with Marketplace", gallery.identifier.id), ExtensionManagementErrorCode.Invalid); + } + + const local = await this.extensionsScanner.extractUserExtension( + extensionKey, + location.fsPath, + { + id: gallery.identifier.uuid, + publisherId: gallery.publisherId, + publisherDisplayName: gallery.publisherDisplayName, + targetPlatform: gallery.properties.targetPlatform, + isApplicationScoped: options.isApplicationScoped, + isMachineScoped: options.isMachineScoped, + isBuiltin: options.isBuiltin, + isPreReleaseVersion: gallery.properties.isPreReleaseVersion, + hasPreReleaseVersion: gallery.properties.isPreReleaseVersion, + installedTimestamp: Date.now(), + pinned: options.installGivenVersion ? true : !!options.pinned, + preRelease: isBoolean(options.preRelease) + ? options.preRelease + : options.installPreReleaseVersion || gallery.properties.isPreReleaseVersion, + source: 'gallery', + }, + false, + token); + return { local, verificationStatus }; + } catch (error) { + try { + await this.extensionsDownloader.delete(location); + } catch (e) { + /* Ignore */ + this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e)); + } + throw toExtensionManagementError(error); + } + } + + private async extractVSIX(extensionKey: ExtensionKey, location: URI, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { + const local = await this.extensionsScanner.extractUserExtension( + extensionKey, + path.resolve(location.fsPath), + { + isApplicationScoped: options.isApplicationScoped, + isMachineScoped: options.isMachineScoped, + isBuiltin: options.isBuiltin, + installedTimestamp: Date.now(), + pinned: options.installGivenVersion ? true : !!options.pinned, + source: 'vsix', + }, + options.keepExisting ?? true, + token); + return { local }; } private async collectFiles(extension: ILocalExtension): Promise { @@ -303,7 +361,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const collectFilesFromDirectory = async (dir: string): Promise => { let entries = await pfs.Promises.readdir(dir); entries = entries.map(e => path.join(dir, e)); - const stats = await Promise.all(entries.map(e => pfs.Promises.stat(e))); + const stats = await Promise.all(entries.map(e => fs.promises.stat(e))); let promise: Promise = Promise.resolve([]); stats.forEach((stat, index) => { const entry = entries[index]; @@ -320,7 +378,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi }; const files = await collectFilesFromDirectory(extension.location.fsPath); - return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f })); + return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f })); } private async onDidChangeExtensionsFromAnotherSource({ added, removed }: DidChangeProfileExtensionsEvent): Promise { @@ -424,6 +482,19 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } +type UpdateMetadataErrorClassification = { + owner: 'sandy081'; + comment: 'Update metadata error'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' }; + code?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; + isProfile?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Is writing into profile' }; +}; +type UpdateMetadataErrorEvent = { + extensionId: string; + code?: string; + isProfile?: boolean; +}; + export class ExtensionsScanner extends Disposable { private readonly uninstalledResource: URI; @@ -432,12 +503,16 @@ export class ExtensionsScanner extends Disposable { private readonly _onExtract = this._register(new Emitter()); readonly onExtract = this._onExtract.event; + private scanAllExtensionPromise = new ResourceMap>(); + private scanUserExtensionsPromise = new ResourceMap>(); + constructor( private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, @IFileService private readonly fileService: IFileService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -451,20 +526,40 @@ export class ExtensionsScanner extends Disposable { } async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; - let scannedExtensions: IScannedExtension[] = []; - if (type === null || type === ExtensionType.System) { - scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); - } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + try { + const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; + let scannedExtensions: IScannedExtension[] = []; + if (type === null || type === ExtensionType.System) { + let scanAllExtensionsPromise = this.scanAllExtensionPromise.get(profileLocation); + if (!scanAllExtensionsPromise) { + scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({ includeInvalid: true, useCache: true }, userScanOptions, false) + .finally(() => this.scanAllExtensionPromise.delete(profileLocation)); + this.scanAllExtensionPromise.set(profileLocation, scanAllExtensionsPromise); + } + scannedExtensions.push(...await scanAllExtensionsPromise); + } else if (type === ExtensionType.User) { + let scanUserExtensionsPromise = this.scanUserExtensionsPromise.get(profileLocation); + if (!scanUserExtensionsPromise) { + scanUserExtensionsPromise = this.extensionsScannerService.scanUserExtensions(userScanOptions) + .finally(() => this.scanUserExtensionsPromise.delete(profileLocation)); + this.scanUserExtensionsPromise.set(profileLocation, scanUserExtensionsPromise); + } + scannedExtensions.push(...await scanUserExtensionsPromise); + } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } - scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async scanAllUserExtensions(excludeOutdated: boolean): Promise { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + try { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); + } } async scanUserExtensionAtLocation(location: URI): Promise { @@ -484,60 +579,67 @@ export class ExtensionsScanner extends Disposable { const tempLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`)); const extensionLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName)); - let exists = await this.fileService.exists(extensionLocation); + if (await this.fileService.exists(extensionLocation)) { + if (!removeIfExists) { + try { + return await this.scanLocalExtension(extensionLocation, ExtensionType.User); + } catch (error) { + this.logService.warn(`Error while scanning the existing extension at ${extensionLocation.path}. Deleting the existing extension and extracting it.`, getErrorMessage(error)); + } + } - if (exists && removeIfExists) { try { await this.deleteExtensionFromLocation(extensionKey.id, extensionLocation, 'removeExisting'); } catch (error) { throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionLocation.fsPath, extensionKey.id), ExtensionManagementErrorCode.Delete); } - exists = false; } - if (exists) { - await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); - } else { - try { - // Extract - try { - this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`); - await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token); - this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id); - } catch (e) { - let errorCode = ExtensionManagementErrorCode.Extract; - if (e instanceof ExtractError) { - if (e.type === 'CorruptZip') { - errorCode = ExtensionManagementErrorCode.CorruptZip; - } else if (e.type === 'Incomplete') { - errorCode = ExtensionManagementErrorCode.IncompleteZip; - } - } - throw new ExtensionManagementError(e.message, errorCode); - } - - await this.extensionsScannerService.updateMetadata(tempLocation, metadata); - - // Rename - try { - this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`); - await this.rename(tempLocation.fsPath, extensionLocation.fsPath); - this.logService.info('Renamed to', extensionLocation.fsPath); - } catch (error) { - if (error.code === 'ENOTEMPTY') { - this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); - } else { - this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempLocation); - throw error; - } - } - - this._onExtract.fire(extensionLocation); - - } catch (error) { - try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } - throw error; + try { + if (token.isCancellationRequested) { + throw new CancellationError(); } + + // Extract + try { + this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`); + await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token); + this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id); + } catch (e) { + throw fromExtractError(e); + } + + try { + await this.extensionsScannerService.updateMetadata(tempLocation, metadata); + } catch (error) { + this.telemetryService.publicLog2('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` }); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + } + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + // Rename + try { + this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`); + await this.rename(tempLocation.fsPath, extensionLocation.fsPath); + this.logService.info('Renamed to', extensionLocation.fsPath); + } catch (error) { + if (error.code === 'ENOTEMPTY') { + this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); + try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } + } else { + this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempLocation); + throw error; + } + } + + this._onExtract.fire(extensionLocation); + + } catch (error) { + try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } + throw error; } return this.scanLocalExtension(extensionLocation, ExtensionType.User); @@ -558,16 +660,25 @@ export class ExtensionsScanner extends Disposable { } async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { - if (profileLocation) { - await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); - } else { - await this.extensionsScannerService.updateMetadata(local.location, metadata); + try { + if (profileLocation) { + await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); + } else { + await this.extensionsScannerService.updateMetadata(local.location, metadata); + } + } catch (error) { + this.telemetryService.publicLog2('extension:extract', { extensionId: local.identifier.id, code: `${toFileOperationResult(error)}`, isProfile: !!profileLocation }); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } return this.scanLocalExtension(local.location, local.type, profileLocation); } - getUninstalledExtensions(): Promise> { - return this.withUninstalledExtensions(); + async getUninstalledExtensions(): Promise> { + try { + return await this.withUninstalledExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); + } } async setUninstalled(...extensions: IExtension[]): Promise { @@ -580,7 +691,11 @@ export class ExtensionsScanner extends Disposable { } async setInstalled(extensionKey: ExtensionKey): Promise { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + try { + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); + } } async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { @@ -630,7 +745,7 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { @@ -666,24 +781,28 @@ export class ExtensionsScanner extends Disposable { try { await pfs.Promises.rename(extractPath, renamePath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Rename); } } - private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { - if (profileLocation) { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); - const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); - } - } else { - const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); + async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { + try { + if (profileLocation) { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); + const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } + } else { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } } + throw new ExtensionManagementError(nls.localize('cannot read', "Cannot read the extension from {0}", location.path), ExtensionManagementErrorCode.ScanningExtension); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ScanningExtension); } - throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path)); } private async toLocalExtension(extension: IScannedExtension): Promise { @@ -705,7 +824,7 @@ export class ExtensionsScanner extends Disposable { isValid: extension.isValid, readmeUrl, changelogUrl, - publisherDisplayName: extension.metadata?.publisherDisplayName || null, + publisherDisplayName: extension.metadata?.publisherDisplayName, publisherId: extension.metadata?.publisherId || null, isApplicationScoped: !!extension.metadata?.isApplicationScoped, isMachineScoped: !!extension.metadata?.isMachineScoped, @@ -791,56 +910,141 @@ export class ExtensionsScanner extends Disposable { } -abstract class InstallExtensionTask extends AbstractExtensionTask implements IInstallExtensionTask { +class InstallExtensionInProfileTask extends AbstractExtensionTask implements IInstallExtensionTask { - private _profileLocation = this.options.profileLocation; - get profileLocation() { return this._profileLocation; } + private _operation = InstallOperation.Install; + get operation() { return this.options.operation ?? this._operation; } - protected _verificationStatus: ExtensionVerificationStatus = false; + private _verificationStatus: ExtensionVerificationStatus | undefined; get verificationStatus() { return this._verificationStatus; } - protected _operation = InstallOperation.Install; - get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; } + readonly identifier: IExtensionIdentifier; constructor( - readonly identifier: IExtensionIdentifier, - readonly source: URI | IGalleryExtension, + private readonly extensionKey: ExtensionKey, + readonly manifest: IExtensionManifest, + readonly source: IGalleryExtension | URI, readonly options: InstallExtensionTaskOptions, - protected readonly extensionsScanner: ExtensionsScanner, - protected readonly uriIdentityService: IUriIdentityService, - protected readonly userDataProfilesService: IUserDataProfilesService, - protected readonly extensionsScannerService: IExtensionsScannerService, - protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, - protected readonly logService: ILogService, + private readonly extractExtensionFn: (operation: InstallOperation, token: CancellationToken) => Promise, + private readonly extensionsScanner: ExtensionsScanner, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, + @ILogService private readonly logService: ILogService, ) { super(); + this.identifier = this.extensionKey.identifier; } - protected override async doRun(token: CancellationToken): Promise { - const [local, metadata] = await this.install(token); - this._profileLocation = local.isBuiltin || local.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : this.options.profileLocation; - if (this.uriIdentityService.extUri.isEqual(this.userDataProfilesService.defaultProfile.extensionsResource, this._profileLocation)) { - await this.extensionsScannerService.initializeDefaultProfileExtensions(); + protected async doRun(token: CancellationToken): Promise { + const installed = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); + const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.identifier)); + if (existingExtension) { + this._operation = InstallOperation.Update; } - await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], this._profileLocation, !local.isValid); - return local; - } - protected async extractExtension({ zipPath, key, metadata }: InstallableExtension, removeIfExists: boolean, token: CancellationToken): Promise { - let local = await this.unsetIfUninstalled(key); - if (local) { - local = await this.extensionsScanner.updateMetadata(local, metadata); - } else { - this.logService.trace('Extracting extension...', key.id); - local = await this.extensionsScanner.extractUserExtension(key, zipPath, metadata, removeIfExists, token); - this.logService.info('Extracting extension completed.', key.id); + const metadata: Metadata = { + isApplicationScoped: this.options.isApplicationScoped || existingExtension?.isApplicationScoped, + isMachineScoped: this.options.isMachineScoped || existingExtension?.isMachineScoped, + isBuiltin: this.options.isBuiltin || existingExtension?.isBuiltin, + isSystem: existingExtension?.type === ExtensionType.System ? true : undefined, + installedTimestamp: Date.now(), + pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), + source: this.source instanceof URI ? 'vsix' : 'gallery', + }; + + let local: ILocalExtension | undefined; + + // VSIX + if (this.source instanceof URI) { + if (existingExtension) { + if (this.extensionKey.equals(new ExtensionKey(existingExtension.identifier, existingExtension.manifest.version))) { + try { + await this.extensionsScanner.removeExtension(existingExtension, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); + } + } + } + // Remove the extension with same version if it is already uninstalled. + // Installing a VSIX extension shall replace the existing extension always. + const existingWithSameVersion = await this.unsetIfUninstalled(this.extensionKey); + if (existingWithSameVersion) { + try { + await this.extensionsScanner.removeExtension(existingWithSameVersion, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); + } + } + } - return local; + + // Gallery + else { + metadata.id = this.source.identifier.uuid; + metadata.publisherId = this.source.publisherId; + metadata.publisherDisplayName = this.source.publisherDisplayName; + metadata.targetPlatform = this.source.properties.targetPlatform; + metadata.updated = !!existingExtension; + metadata.isPreReleaseVersion = this.source.properties.isPreReleaseVersion; + metadata.hasPreReleaseVersion = existingExtension?.hasPreReleaseVersion || this.source.properties.isPreReleaseVersion; + metadata.preRelease = isBoolean(this.options.preRelease) + ? this.options.preRelease + : this.options.installPreReleaseVersion || this.source.properties.isPreReleaseVersion || existingExtension?.preRelease; + + if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.source.version) { + return this.extensionsScanner.updateMetadata(existingExtension, metadata, this.options.profileLocation); + } + + // Unset if the extension is uninstalled and return the unset extension. + local = await this.unsetIfUninstalled(this.extensionKey); + } + + if (token.isCancellationRequested) { + throw toExtensionManagementError(new CancellationError()); + } + + if (!local) { + const result = await this.extractExtensionFn(this.operation, token); + local = result.local; + this._verificationStatus = result.verificationStatus; + } + + if (this.uriIdentityService.extUri.isEqual(this.userDataProfilesService.defaultProfile.extensionsResource, this.options.profileLocation)) { + try { + await this.extensionsScannerService.initializeDefaultProfileExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.IntializeDefaultProfile); + } + } + + if (token.isCancellationRequested) { + throw toExtensionManagementError(new CancellationError()); + } + + try { + await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], this.options.profileLocation, !local.isValid); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.AddToProfile); + } + + const result = await this.extensionsScanner.scanLocalExtension(local.location, ExtensionType.User, this.options.profileLocation); + if (!result) { + throw new ExtensionManagementError('Cannot find the installed extension', ExtensionManagementErrorCode.InstalledExtensionNotFound); + } + + if (this.source instanceof URI) { + this.updateMetadata(local, token); + } + + return result; } - protected async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { - const isUninstalled = await this.isUninstalled(extensionKey); - if (!isUninstalled) { + private async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + if (!uninstalled[extensionKey.toString()]) { return undefined; } @@ -853,202 +1057,6 @@ abstract class InstallExtensionTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } - private async isUninstalled(extensionId: ExtensionKey): Promise { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - return !!uninstalled[extensionId.toString()]; - } - - protected abstract install(token: CancellationToken): Promise<[ILocalExtension, Metadata]>; - -} - -export class InstallGalleryExtensionTask extends InstallExtensionTask { - - constructor( - manifest: IExtensionManifest, - private readonly gallery: IGalleryExtension, - options: InstallExtensionTaskOptions, - private readonly extensionsDownloader: ExtensionsDownloader, - extensionsScanner: ExtensionsScanner, - uriIdentityService: IUriIdentityService, - userDataProfilesService: IUserDataProfilesService, - extensionsScannerService: IExtensionsScannerService, - extensionsProfileScannerService: IExtensionsProfileScannerService, - logService: ILogService, - private readonly telemetryService: ITelemetryService, - ) { - super(gallery.identifier, gallery, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService); - } - - protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - let installed; - try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); - } catch (error) { - throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); - } - - const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.gallery.identifier)); - if (existingExtension) { - this._operation = InstallOperation.Update; - } - - const metadata: Metadata = { - id: this.gallery.identifier.uuid, - publisherId: this.gallery.publisherId, - publisherDisplayName: this.gallery.publisherDisplayName, - targetPlatform: this.gallery.properties.targetPlatform, - isApplicationScoped: this.options.isApplicationScoped || existingExtension?.isApplicationScoped, - isMachineScoped: this.options.isMachineScoped || existingExtension?.isMachineScoped, - isBuiltin: this.options.isBuiltin || existingExtension?.isBuiltin, - isSystem: existingExtension?.type === ExtensionType.System ? true : undefined, - updated: !!existingExtension, - isPreReleaseVersion: this.gallery.properties.isPreReleaseVersion, - hasPreReleaseVersion: existingExtension?.hasPreReleaseVersion || this.gallery.properties.isPreReleaseVersion, - installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), - preRelease: isBoolean(this.options.preRelease) - ? this.options.preRelease - : this.options.installPreReleaseVersion || this.gallery.properties.isPreReleaseVersion || existingExtension?.preRelease, - source: 'gallery', - }; - - if (existingExtension?.manifest.version === this.gallery.version) { - try { - const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); - return [local, metadata]; - } catch (error) { - throw new ExtensionManagementError(getErrorMessage(error), ExtensionManagementErrorCode.UpdateMetadata); - } - } - - try { - return await this.downloadAndInstallExtension(metadata, token); - } catch (error) { - if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.CorruptZip || error.code === ExtensionManagementErrorCode.IncompleteZip)) { - this.logService.info(`Downloaded VSIX is invalid. Trying to download and install again...`, this.gallery.identifier.id); - type RetryInstallingInvalidVSIXClassification = { - owner: 'sandy081'; - comment: 'Event reporting the retry of installing an invalid VSIX'; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; - succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; - }; - type RetryInstallingInvalidVSIXEvent = { - extensionId: string; - succeeded: boolean; - }; - try { - const result = await this.downloadAndInstallExtension(metadata, token); - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: true - }); - return result; - } catch (error) { - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: false - }); - throw error; - } - } else { - throw error; - } - } - } - - private async downloadAndInstallExtension(metadata: Metadata, token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const { location, verificationStatus } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); - try { - this._verificationStatus = verificationStatus; - this.validateManifest(location.fsPath); - const local = await this.extractExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, false, token); - return [local, metadata]; - } catch (error) { - try { - await this.extensionsDownloader.delete(location); - } catch (error) { - /* Ignore */ - this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error)); - } - throw error; - } - } - - protected async validateManifest(zipPath: string): Promise { - try { - await getManifest(zipPath); - } catch (error) { - throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid); - } - } - -} - -class InstallVSIXTask extends InstallExtensionTask { - - constructor( - private readonly manifest: IExtensionManifest, - private readonly location: URI, - options: InstallExtensionTaskOptions, - private readonly galleryService: IExtensionGalleryService, - extensionsScanner: ExtensionsScanner, - uriIdentityService: IUriIdentityService, - userDataProfilesService: IUserDataProfilesService, - extensionsScannerService: IExtensionsScannerService, - extensionsProfileScannerService: IExtensionsProfileScannerService, - logService: ILogService, - ) { - super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService); - } - - protected override async doRun(token: CancellationToken): Promise { - const local = await super.doRun(token); - this.updateMetadata(local, token); - return local; - } - - protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const extensionKey = new ExtensionKey(this.identifier, this.manifest.version); - const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); - const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); - const metadata: Metadata = { - isApplicationScoped: this.options.isApplicationScoped || existing?.isApplicationScoped, - isMachineScoped: this.options.isMachineScoped || existing?.isMachineScoped, - isBuiltin: this.options.isBuiltin || existing?.isBuiltin, - installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existing?.pinned), - source: 'vsix', - }; - - if (existing) { - this._operation = InstallOperation.Update; - if (extensionKey.equals(new ExtensionKey(existing.identifier, existing.manifest.version))) { - try { - await this.extensionsScanner.removeExtension(existing, 'existing'); - } catch (e) { - throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); - } - } else if (!this.options.profileLocation && semver.gt(existing.manifest.version, this.manifest.version)) { - await this.extensionsScanner.setUninstalled(existing); - } - } else { - // Remove the extension with same version if it is already uninstalled. - // Installing a VSIX extension shall replace the existing extension always. - const existing = await this.unsetIfUninstalled(extensionKey); - if (existing) { - try { - await this.extensionsScanner.removeExtension(existing, 'existing'); - } catch (e) { - throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); - } - } - } - - const local = await this.extractExtension({ zipPath: path.resolve(this.location.fsPath), key: extensionKey, metadata }, true, token); - return [local, metadata]; - } - private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise { try { let [galleryExtension] = await this.galleryService.getExtensions([{ id: extension.identifier.id, version: extension.manifest.version }], token); @@ -1072,19 +1080,18 @@ class InstallVSIXTask extends InstallExtensionTask { } } -class UninstallExtensionTask extends AbstractExtensionTask implements IUninstallExtensionTask { +class UninstallExtensionInProfileTask extends AbstractExtensionTask implements IUninstallExtensionTask { constructor( readonly extension: ILocalExtension, - private readonly profileLocation: URI, + readonly options: UninstallExtensionTaskOptions, private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, ) { super(); } protected async doRun(token: CancellationToken): Promise { - await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.profileLocation); + await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); } } - diff --git a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts index 6d9a54272da..96118542408 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts @@ -3,17 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { buffer } from 'vs/base/node/zip'; +import { buffer, ExtractError } from 'vs/base/node/zip'; import { localize } from 'vs/nls'; +import { toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -export function getManifest(vsix: string): Promise { - return buffer(vsix, 'extension/package.json') - .then(buffer => { - try { - return JSON.parse(buffer.toString('utf8')); - } catch (err) { - throw new Error(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file.")); - } - }); +export function fromExtractError(e: Error): ExtensionManagementError { + let errorCode = ExtensionManagementErrorCode.Extract; + if (e instanceof ExtractError) { + if (e.type === 'CorruptZip') { + errorCode = ExtensionManagementErrorCode.CorruptZip; + } else if (e.type === 'Incomplete') { + errorCode = ExtensionManagementErrorCode.IncompleteZip; + } + } + return toExtensionManagementError(e, errorCode); +} + +export async function getManifest(vsixPath: string): Promise { + let data; + try { + data = await buffer(vsixPath, 'extension/package.json'); + } catch (e) { + throw fromExtractError(e); + } + + try { + return JSON.parse(data.toString('utf8')); + } catch (err) { + throw new ExtensionManagementError(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file."), ExtensionManagementErrorCode.Invalid); + } } diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index 05d218b50b5..8d1c7fb8679 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -3,9 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { importAMDNodeModule } from 'vs/amdX'; import { getErrorMessage } from 'vs/base/common/errors'; +import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IExtensionSignatureVerificationService = createDecorator('IExtensionSignatureVerificationService'); @@ -18,30 +21,69 @@ export interface IExtensionSignatureVerificationService { /** * Verifies an extension file (.vsix) against a signature archive file. + * @param { string } extensionId The extension identifier. * @param { string } vsixFilePath The extension file path. * @param { string } signatureArchiveFilePath The signature archive file path. - * @param { boolean } verbose A flag indicating whether or not to capture verbose detail in the event of an error. * @returns { Promise } A promise with `true` if the extension is validly signed and trusted; * otherwise, `false` because verification is not enabled (e.g.: in the OSS version of VS Code). * @throws { ExtensionSignatureVerificationError } An error with a code indicating the validity, integrity, or trust issue * found during verification or a more fundamental issue (e.g.: a required dependency was not found). */ - verify(vsixFilePath: string, signatureArchiveFilePath: string, verbose: boolean): Promise; + verify(extension: IGalleryExtension, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise; } declare module vsceSign { - export function verify(vsixFilePath: string, signatureArchiveFilePath: string, verbose: boolean): Promise; + export function verify(vsixFilePath: string, signatureArchiveFilePath: string, verbose: boolean): Promise; +} + +export enum ExtensionSignatureVerificationCode { + 'Success' = 'Success', + 'RequiredArgumentMissing' = 'RequiredArgumentMissing', + 'InvalidArgument' = 'InvalidArgument', + 'PackageIsUnreadable' = 'PackageIsUnreadable', + 'UnhandledException' = 'UnhandledException', + 'SignatureManifestIsMissing' = 'SignatureManifestIsMissing', + 'SignatureManifestIsUnreadable' = 'SignatureManifestIsUnreadable', + 'SignatureIsMissing' = 'SignatureIsMissing', + 'SignatureIsUnreadable' = 'SignatureIsUnreadable', + 'CertificateIsUnreadable' = 'CertificateIsUnreadable', + 'SignatureArchiveIsUnreadable' = 'SignatureArchiveIsUnreadable', + 'FileAlreadyExists' = 'FileAlreadyExists', + 'SignatureArchiveIsInvalidZip' = 'SignatureArchiveIsInvalidZip', + 'SignatureArchiveHasSameSignatureFile' = 'SignatureArchiveHasSameSignatureFile', + + 'PackageIntegrityCheckFailed' = 'PackageIntegrityCheckFailed', + 'SignatureIsInvalid' = 'SignatureIsInvalid', + 'SignatureManifestIsInvalid' = 'SignatureManifestIsInvalid', + 'SignatureIntegrityCheckFailed' = 'SignatureIntegrityCheckFailed', + 'EntryIsMissing' = 'EntryIsMissing', + 'EntryIsTampered' = 'EntryIsTampered', + 'Untrusted' = 'Untrusted', + 'CertificateRevoked' = 'CertificateRevoked', + 'SignatureIsNotValid' = 'SignatureIsNotValid', + 'UnknownError' = 'UnknownError', + 'PackageIsInvalidZip' = 'PackageIsInvalidZip', + 'SignatureArchiveHasTooManyEntries' = 'SignatureArchiveHasTooManyEntries', } /** - * An error raised during extension signature verification. + * Extension signature verification result */ -export interface ExtensionSignatureVerificationError extends Error { - readonly code: string; +export interface ExtensionSignatureVerificationResult { + readonly code: ExtensionSignatureVerificationCode; readonly didExecute: boolean; + readonly internalCode?: number; readonly output?: string; } +export class ExtensionSignatureVerificationError extends Error { + constructor( + public readonly code: ExtensionSignatureVerificationCode, + ) { + super(code); + } +} + export class ExtensionSignatureVerificationService implements IExtensionSignatureVerificationService { declare readonly _serviceBrand: undefined; @@ -54,58 +96,77 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur private vsceSign(): Promise { if (!this.moduleLoadingPromise) { - this.moduleLoadingPromise = new Promise( - (resolve, reject) => require( - ['@vscode/vsce-sign'], - async (obj) => { - const instance = obj; - - return resolve(instance); - }, reject)); + this.moduleLoadingPromise = importAMDNodeModule('@vscode/vsce-sign', 'src/main.js'); } return this.moduleLoadingPromise; } - public async verify(vsixFilePath: string, signatureArchiveFilePath: string, verbose: boolean): Promise { + public async verify(extension: IGalleryExtension, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise { let module: typeof vsceSign; + const extensionId = extension.identifier.id; try { module = await this.vsceSign(); } catch (error) { this.logService.error('Could not load vsce-sign module', getErrorMessage(error)); + this.logService.info(`Extension signature verification is not done: ${extensionId}`); return false; } const startTime = new Date().getTime(); - let verified: boolean | undefined; - let error: ExtensionSignatureVerificationError | undefined; + let result: ExtensionSignatureVerificationResult; try { - verified = await module.verify(vsixFilePath, signatureArchiveFilePath, verbose); - return verified; + this.logService.trace(`Verifying extension signature for ${extensionId}...`); + result = await module.verify(vsixFilePath, signatureArchiveFilePath, this.logService.getLevel() === LogLevel.Trace); } catch (e) { - error = e; - throw e; - } finally { - const duration = new Date().getTime() - startTime; - type ExtensionSignatureVerificationClassification = { - owner: 'sandy081'; - comment: 'Extension signature verification event'; - duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken to verify the signature' }; - verified?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'verified status when succeeded' }; - error?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code when failed' }; + result = { + code: ExtensionSignatureVerificationCode.UnknownError, + didExecute: false, + output: getErrorMessage(e) }; - type ExtensionSignatureVerificationEvent = { - duration: number; - verified?: boolean; - error?: string; - }; - this.telemetryService.publicLog2('extensionsignature:verification', { - duration, - verified, - error: error ? (error.code ?? 'unknown') : undefined, - }); } + + const duration = new Date().getTime() - startTime; + + this.logService.info(`Extension signature verification result for ${extensionId}: ${result.code}. Executed: ${result.didExecute}. Duration: ${duration}ms.`); + this.logService.trace(`Extension signature verification output for ${extensionId}:\n${result.output}`); + + type ExtensionSignatureVerificationClassification = { + owner: 'sandy081'; + comment: 'Extension signature verification event'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension version' }; + code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'result code of the verification' }; + internalCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'internal code of the verification' }; + duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken to verify the signature' }; + didExecute: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'whether the verification was executed' }; + clientTargetPlatform?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'target platform of the client' }; + }; + type ExtensionSignatureVerificationEvent = { + extensionId: string; + extensionVersion: string; + code: string; + internalCode?: number; + duration: number; + didExecute: boolean; + clientTargetPlatform?: string; + }; + this.telemetryService.publicLog2('extensionsignature:verification', { + extensionId, + extensionVersion: extension.version, + code: result.code, + internalCode: result.internalCode, + duration, + didExecute: result.didExecute, + clientTargetPlatform, + }); + + if (result.code === ExtensionSignatureVerificationCode.Success) { + return true; + } + + throw new ExtensionSignatureVerificationError(result.code); } } diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index ba8e026b893..a56087b0b51 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getErrorMessage } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { combinedDisposable, Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import { ResourceSet } from 'vs/base/common/map'; @@ -40,7 +41,7 @@ export class ExtensionsWatcher extends Disposable { private readonly logService: ILogService, ) { super(); - this.initialize().then(null, error => logService.error(error)); + this.initialize().then(null, error => logService.error('Error while initializing Extensions Watcher', getErrorMessage(error))); } private async initialize(): Promise { diff --git a/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts b/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts index ce93c6e73d7..178293d8874 100644 --- a/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts +++ b/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getDomainsOfRemotes, getRemotes } from 'vs/platform/extensionManagement/common/configRemotes'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index cebafab4714..e4f187c323c 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { isUUID } from 'vs/base/common/uuid'; @@ -52,6 +52,7 @@ suite('Extension Gallery Service', () => { test('marketplace machine id', async () => { const headers = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService, NullTelemetryService); + assert.ok(headers['X-Market-User-Id']); assert.ok(isUUID(headers['X-Market-User-Id'])); const headers2 = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService, NullTelemetryService); assert.strictEqual(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); diff --git a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts index d0813771c18..1c3d62f7f0b 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts index a3c2603a1eb..64aad4fbd41 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { deepClone } from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILocalizedString } from 'vs/platform/action/common/action'; +import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; -import { IExtensionManifest, IConfiguration } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { NullLogger } from 'vs/platform/log/common/log'; const manifest: IExtensionManifest = { @@ -62,7 +63,7 @@ suite('Localize Manifest', () => { assert.strictEqual(localizedManifest.contributes?.commands?.[0].title, 'Test Command'); assert.strictEqual(localizedManifest.contributes?.commands?.[0].category, 'Test Category'); assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Test Authentication'); - assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Test Configuration'); + assert.strictEqual((localizedManifest.contributes?.configuration as IConfigurationNode).title, 'Test Configuration'); }); test('replaces template strings with fallback if not found in translations', function () { @@ -81,7 +82,7 @@ suite('Localize Manifest', () => { assert.strictEqual(localizedManifest.contributes?.commands?.[0].title, 'Test Command'); assert.strictEqual(localizedManifest.contributes?.commands?.[0].category, 'Test Category'); assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Test Authentication'); - assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Test Configuration'); + assert.strictEqual((localizedManifest.contributes?.configuration as IConfigurationNode).title, 'Test Configuration'); }); test('replaces template strings - command title & categories become ILocalizedString', function () { @@ -111,7 +112,7 @@ suite('Localize Manifest', () => { // Everything else stays as a string. assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Testauthentifizierung'); - assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Testkonfiguration'); + assert.strictEqual((localizedManifest.contributes?.configuration as IConfigurationNode).title, 'Testkonfiguration'); }); test('replaces template strings - is best effort #164630', function () { diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 5e71b85714e..d94305a83fc 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { VSBuffer } from 'vs/base/common/buffer'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts new file mode 100644 index 00000000000..bae93bde803 --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/buffer'; +import { platform } from 'vs/base/common/platform'; +import { arch } from 'vs/base/common/process'; +import { joinPath } from 'vs/base/common/resources'; +import { isBoolean } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; +import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; + +const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + +class TestExtensionSignatureVerificationService extends mock() { + + constructor( + private readonly verificationResult: string | boolean) { + super(); + } + + override async verify(): Promise { + if (isBoolean(this.verificationResult)) { + return this.verificationResult; + } + const error = Error(this.verificationResult); + (error as any).code = this.verificationResult; + throw error; + } +} + +class TestExtensionDownloader extends ExtensionsDownloader { + protected override async validate(): Promise { } +} + +suite('ExtensionDownloader Tests', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = disposables.add(new TestInstantiationService()); + + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); + + instantiationService.stub(ILogService, logService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(ILogService, logService); + instantiationService.stub(INativeEnvironmentService, { extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') }); + instantiationService.stub(IExtensionGalleryService, { + async download(extension, location, operation) { + await fileService.writeFile(location, VSBuffer.fromString('extension vsix')); + }, + async downloadSignatureArchive(extension, location) { + await fileService.writeFile(location, VSBuffer.fromString('extension signature')); + }, + }); + }); + + test('download completes successfully if verification is disabled by setting set to false', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: false, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification is disabled by options', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, false); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification is disabled because the module is not loaded', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: false }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification fails to execute', async () => { + const errorCode = 'ENOENT'; + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, errorCode); + }); + + test('download completes successfully if verification fails ', async () => { + const errorCode = 'IntegrityCheckFailed'; + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, errorCode); + }); + + test('download completes successfully if verification succeeds', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, true); + }); + + test('download completes successfully for unsigned extension', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully for an unsigned extension even when signature verification throws error', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + function aTestObject(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string }): ExtensionsDownloader { + instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); + instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); + return disposables.add(instantiationService.createInstance(TestExtensionDownloader)); + } + + function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { + const targetPlatform = getTargetPlatform(platform, arch); + const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); + galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; + galleryExtension.assets = { ...galleryExtension.assets, ...assets }; + galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; + return galleryExtension; + } +}); diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 9e72c1c174a..8c0569fe67b 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts deleted file mode 100644 index 1b27c8504d8..00000000000 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { platform } from 'vs/base/common/platform'; -import { arch } from 'vs/base/common/process'; -import { joinPath } from 'vs/base/common/resources'; -import { isBoolean } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { mock } from 'vs/base/test/common/mock'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; -import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; -import { ExtensionsScanner, InstallGalleryExtensionTask } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; -import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; -import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { ILogService, NullLogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; - -const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); - -class TestExtensionsScanner extends mock() { - override async scanExtensions(): Promise { return []; } -} - -class TestExtensionSignatureVerificationService extends mock() { - - constructor( - private readonly verificationResult: string | boolean, - private readonly didExecute: boolean) { - super(); - } - - override async verify(): Promise { - if (isBoolean(this.verificationResult)) { - return this.verificationResult; - } - const error = Error(this.verificationResult); - (error as any).code = this.verificationResult; - (error as any).didExecute = this.didExecute; - throw error; - } -} - -class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { - - installed = false; - - constructor( - extension: IGalleryExtension, - extensionDownloader: ExtensionsDownloader, - disposables: DisposableStore, - ) { - const instantiationService = disposables.add(new TestInstantiationService()); - const logService = instantiationService.stub(ILogService, new NullLogService()); - const fileService = instantiationService.stub(IFileService, disposables.add(new FileService(logService))); - const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - const systemExtensionsLocation = joinPath(ROOT, 'system'); - const userExtensionsLocation = joinPath(ROOT, 'extensions'); - instantiationService.stub(INativeEnvironmentService, { - userHome: ROOT, - userRoamingDataHome: ROOT, - builtinExtensionsPath: systemExtensionsLocation.fsPath, - extensionsPath: userExtensionsLocation.fsPath, - userDataPath: userExtensionsLocation.fsPath, - cacheHome: ROOT, - }); - instantiationService.stub(IProductService, {}); - instantiationService.stub(ITelemetryService, NullTelemetryService); - const uriIdentityService = instantiationService.stub(IUriIdentityService, disposables.add(instantiationService.createInstance(UriIdentityService))); - const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, disposables.add(instantiationService.createInstance(UserDataProfilesService))); - const extensionsProfileScannerService = instantiationService.stub(IExtensionsProfileScannerService, disposables.add(instantiationService.createInstance(ExtensionsProfileScannerService))); - const extensionsScannerService = instantiationService.stub(IExtensionsScannerService, disposables.add(instantiationService.createInstance(ExtensionsScannerService))); - super( - { - name: extension.name, - publisher: extension.publisher, - version: extension.version, - engines: { vscode: '*' }, - }, - extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, - extensionDownloader, - new TestExtensionsScanner(), - uriIdentityService, - userDataProfilesService, - extensionsScannerService, - extensionsProfileScannerService, - logService, - NullTelemetryService - ); - } - - protected override async doRun(token: CancellationToken): Promise { - const result = await this.install(token); - return result[0]; - } - - protected override async extractExtension(): Promise { - this.installed = true; - return new class extends mock() { }; - } - - protected override async validateManifest(): Promise { } -} - -suite('InstallGalleryExtensionTask Tests', () => { - - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - test('if verification is enabled by default, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is enabled in stable, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true, quality: 'stable' }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is disabled by setting set to false, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: false, verificationResult: 'error', didExecute: false }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is disabled because the module is not loaded, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: false, didExecute: false }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification fails to execute, the task completes', async () => { - const errorCode = 'ENOENT'; - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode, didExecute: false }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, errorCode); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification fails', async () => { - const errorCode = 'IntegrityCheckFailed'; - - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode, didExecute: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, errorCode); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification succeeds, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('task completes for unsigned extension', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, didExecute: false }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('task completes for an unsigned extension even when signature verification throws error', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: 'error', didExecute: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - function anExtensionsDownloader(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string; didExecute: boolean; quality?: string }): ExtensionsDownloader { - const logService = new NullLogService(); - const fileService = disposables.add(new FileService(logService)); - const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - - const instantiationService = disposables.add(new TestInstantiationService()); - instantiationService.stub(IProductService, { quality: options.quality ?? 'insiders' }); - instantiationService.stub(IFileService, fileService); - instantiationService.stub(ILogService, logService); - instantiationService.stub(INativeEnvironmentService, { extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') }); - instantiationService.stub(IExtensionGalleryService, { - async download(extension, location, operation) { - await fileService.writeFile(location, VSBuffer.fromString('extension vsix')); - }, - async downloadSignatureArchive(extension, location) { - await fileService.writeFile(location, VSBuffer.fromString('extension signature')); - }, - }); - instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); - instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult, !!options.didExecute)); - return disposables.add(instantiationService.createInstance(ExtensionsDownloader)); - } - - function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { - const targetPlatform = getTargetPlatform(platform, arch); - const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); - galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; - galleryExtension.assets = { ...galleryExtension.assets, ...assets }; - galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; - return galleryExtension; - } -}); diff --git a/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts b/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts index f1660961c58..120b996402a 100644 --- a/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts +++ b/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts @@ -6,7 +6,6 @@ import { isWeb } from 'vs/base/common/platform'; import { format2 } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IHeaders } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -110,8 +109,8 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi return !!this._extensionGalleryAuthority && this._extensionGalleryAuthority === this._getExtensionGalleryAuthority(uri); } - protected async getExtensionGalleryRequestHeaders(): Promise { - const headers: IHeaders = { + protected async getExtensionGalleryRequestHeaders(): Promise> { + const headers: Record = { 'X-Client-Name': `${this._productService.applicationName}${isWeb ? '-web' : ''}`, 'X-Client-Version': this._productService.version }; diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index cee5eaeedef..5fae8b4b1e2 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -8,7 +8,8 @@ import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, parseApiProposals } from 'vs/platform/extensions/common/extensions'; +import { allApiProposals } from 'vs/platform/extensions/common/extensionsApiProposals'; export interface IParsedVersion { hasCaret: boolean; @@ -239,7 +240,7 @@ export function isValidVersion(_inputVersion: string | INormalizedVersion, _inpu type ProductDate = string | Date | undefined; -export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): readonly [Severity, string][] { +export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, validateApiVersion: boolean): readonly [Severity, string][] { const validations: [Severity, string][] = []; if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') { validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]); @@ -314,12 +315,22 @@ export function validateExtensionManifest(productVersion: string, productDate: P } const notices: string[] = []; - const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); - if (!isValid) { + const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); + if (!validExtensionVersion) { for (const notice of notices) { validations.push([Severity.Error, notice]); } } + + if (validateApiVersion && extensionManifest.enabledApiProposals?.length) { + const incompatibleNotices: string[] = []; + if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) { + for (const notice of incompatibleNotices) { + validations.push([Severity.Error, notice]); + } + } + } + return validations; } @@ -338,6 +349,34 @@ export function isEngineValid(engine: string, version: string, date: ProductDate return engine === '*' || isVersionValid(version, date, engine); } +export function areApiProposalsCompatible(apiProposals: string[]): boolean; +export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean; +export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean; +export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): boolean { + if (apiProposals.length === 0) { + return true; + } + const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined; + const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (notices ? undefined : arg1) ?? allApiProposals; + const incompatibleNotices: string[] = []; + const parsedProposals = parseApiProposals(apiProposals); + for (const { proposalName, version } of parsedProposals) { + const existingProposal = productApiProposals[proposalName]; + if (!existingProposal) { + continue; + } + if (!version) { + continue; + } + if (existingProposal.version !== version) { + incompatibleNotices.push(nls.localize('apiProposalMismatch', "Extension is using an API proposal '{0}' that is not compatible with the current version of VS Code.", proposalName)); + } + } + notices?.push(...incompatibleNotices); + return incompatibleNotices.length === 0; + +} + function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean { const desiredVersion = normalizeVersion(parseVersion(requestedVersion)); diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 331aba1b55f..822260bdc2f 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -21,19 +21,6 @@ export interface ICommand { category?: string | ILocalizedString; } -export interface IConfigurationProperty { - description: string; - type: string | string[]; - default?: any; -} - -export interface IConfiguration { - id?: string; - order?: number; - title?: string; - properties: { [key: string]: IConfigurationProperty }; -} - export interface IDebugger { label?: string; type: string; @@ -41,7 +28,7 @@ export interface IDebugger { } export interface IGrammar { - language: string; + language?: string; } export interface IJSONValidation { @@ -182,7 +169,7 @@ export interface ILocalizationContribution { export interface IExtensionContributions { commands?: ICommand[]; - configuration?: IConfiguration | IConfiguration[]; + configuration?: any; debuggers?: IDebugger[]; grammars?: IGrammar[]; jsonValidation?: IJSONValidation[]; @@ -205,6 +192,7 @@ export interface IExtensionContributions { readonly notebooks?: INotebookEntry[]; readonly notebookRenderer?: INotebookRendererContribution[]; readonly debugVisualizers?: IDebugVisualizationContribution[]; + readonly chatParticipants?: ReadonlyArray<{ id: string }>; } export interface IExtensionCapabilities { @@ -238,7 +226,9 @@ export interface IExtensionIdentifier { } export const EXTENSION_CATEGORIES = [ + 'AI', 'Azure', + 'Chat', 'Data Science', 'Debuggers', 'Extension Packs', @@ -255,8 +245,6 @@ export const EXTENSION_CATEGORIES = [ 'Testing', 'Themes', 'Visualization', - 'AI', - 'Chat', 'Other', ]; @@ -283,6 +271,7 @@ export interface IRelaxedExtensionManifest { contributes?: IExtensionContributions; repository?: { url: string }; bugs?: { url: string }; + originalEnabledApiProposals?: readonly string[]; enabledApiProposals?: readonly string[]; api?: string; scripts?: { [key: string]: string }; @@ -324,6 +313,7 @@ export interface IExtension { readonly manifest: IExtensionManifest; readonly location: URI; readonly targetPlatform: TargetPlatform; + readonly publisherDisplayName?: string; readonly readmeUrl?: URI; readonly changelogUrl?: URI; readonly isValid: boolean; @@ -460,6 +450,7 @@ export interface IRelaxedExtensionDescription extends IRelaxedExtensionManifest id?: string; identifier: ExtensionIdentifier; uuid?: string; + publisherDisplayName?: string; targetPlatform: TargetPlatform; isBuiltin: boolean; isUserBuiltin: boolean; @@ -489,6 +480,17 @@ export function isResolverExtension(manifest: IExtensionManifest, remoteAuthorit return false; } +export function parseApiProposals(enabledApiProposals: string[]): { proposalName: string; version?: number }[] { + return enabledApiProposals.map(proposal => { + const [proposalName, version] = proposal.split('@'); + return { proposalName, version: version ? parseInt(version) : undefined }; + }); +} + +export function parseEnabledApiProposalNames(enabledApiProposals: string[]): string[] { + return enabledApiProposals.map(proposal => proposal.split('@')[0]); +} + export const IBuiltinExtensionsScannerService = createDecorator('IBuiltinExtensionsScannerService'); export interface IBuiltinExtensionsScannerService { readonly _serviceBrand: undefined; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts new file mode 100644 index 00000000000..463e0a92727 --- /dev/null +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -0,0 +1,403 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. + +const _allApiProposals = { + activeComment: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.activeComment.d.ts', + }, + aiRelatedInformation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', + }, + aiTextSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', + }, + aiTextSearchProviderNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProviderNew.d.ts', + }, + attributableCoverage: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts', + }, + authLearnMore: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', + }, + authSession: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', + }, + canonicalUriProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', + }, + chatParticipantAdditions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', + }, + chatParticipantPrivate: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', + version: 2 + }, + chatProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', + }, + chatTab: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', + }, + chatVariableResolver: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts', + }, + codeActionAI: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionAI.d.ts', + }, + codeActionRanges: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionRanges.d.ts', + }, + codiconDecoration: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', + }, + commentReactor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', + }, + commentReveal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReveal.d.ts', + }, + commentThreadApplicability: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts', + }, + commentingRangeHint: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentingRangeHint.d.ts', + }, + commentsDraftState: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', + }, + contribAccessibilityHelpContent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribAccessibilityHelpContent.d.ts', + }, + contribCommentEditorActionsMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', + }, + contribCommentPeekContext: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', + }, + contribCommentThreadAdditionalMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', + }, + contribCommentsViewThreadMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts', + }, + contribDiffEditorGutterToolBarMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts', + }, + contribEditSessions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', + }, + contribEditorContentMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', + }, + contribIssueReporter: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts', + }, + contribLabelFormatterWorkspaceTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', + }, + contribMenuBarHome: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', + }, + contribMergeEditorMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts', + }, + contribMultiDiffEditorMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMultiDiffEditorMenus.d.ts', + }, + contribNotebookStaticPreloads: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribNotebookStaticPreloads.d.ts', + }, + contribRemoteHelp: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', + }, + contribShareMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', + }, + contribSourceControlHistoryItemChangesMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemChangesMenu.d.ts', + }, + contribSourceControlHistoryItemGroupMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemGroupMenu.d.ts', + }, + contribSourceControlHistoryItemMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemMenu.d.ts', + }, + contribSourceControlInputBoxMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts', + }, + contribSourceControlTitleMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlTitleMenu.d.ts', + }, + contribStatusBarItems: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', + }, + contribViewContainerTitle: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewContainerTitle.d.ts', + }, + contribViewsRemote: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', + }, + contribViewsWelcome: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', + }, + createFileSystemWatcher: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts', + }, + customEditorMove: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', + }, + debugVisualization: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', + }, + defaultChatParticipant: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts', + }, + diffCommand: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', + }, + diffContentOptions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', + }, + documentFiltersExclusive: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', + }, + documentPaste: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', + }, + editSessionIdentityProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', + }, + editorHoverVerbosityLevel: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts', + }, + editorInsets: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', + }, + embeddings: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts', + }, + extensionRuntime: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', + }, + extensionsAny: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', + }, + externalUriOpener: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', + }, + fileComments: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileComments.d.ts', + }, + fileSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', + }, + fileSearchProviderNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProviderNew.d.ts', + }, + findFiles2: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts', + }, + findFiles2New: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2New.d.ts', + }, + findTextInFiles: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', + }, + findTextInFilesNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFilesNew.d.ts', + }, + fsChunks: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', + }, + idToken: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', + }, + inlineCompletionsAdditions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', + }, + inlineEdit: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineEdit.d.ts', + }, + interactive: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', + }, + interactiveWindow: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', + }, + ipc: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', + }, + languageModelSystem: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', + }, + languageStatusText: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', + }, + lmTools: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.lmTools.d.ts', + version: 3 + }, + mappedEditsProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', + }, + multiDocumentHighlightProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', + }, + newSymbolNamesProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.newSymbolNamesProvider.d.ts', + }, + notebookCellExecution: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts', + }, + notebookCellExecutionState: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', + }, + notebookControllerAffinityHidden: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', + }, + notebookDeprecated: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', + }, + notebookExecution: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookExecution.d.ts', + }, + notebookKernelSource: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts', + }, + notebookLiveShare: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', + }, + notebookMessaging: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', + }, + notebookMime: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', + }, + notebookVariableProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts', + }, + portsAttributes: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', + }, + profileContentHandlers: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', + }, + quickDiffProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', + }, + quickInputButtonLocation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts', + }, + quickPickItemTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', + }, + quickPickSortByLabel: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', + }, + resolvers: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', + }, + scmActionButton: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', + }, + scmHistoryProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts', + }, + scmMultiDiffEditor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts', + }, + scmSelectedProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', + }, + scmTextDocument: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts', + }, + scmValidation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', + }, + shareProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.shareProvider.d.ts', + }, + showLocal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.showLocal.d.ts', + }, + speech: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.speech.d.ts', + }, + tabInputMultiDiff: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts', + }, + tabInputTextMerge: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', + }, + taskPresentationGroup: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', + }, + telemetry: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', + }, + terminalDataWriteEvent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', + }, + terminalDimensions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', + }, + terminalExecuteCommandEvent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts', + }, + terminalQuickFixProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', + }, + terminalSelection: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', + }, + testMessageStackTrace: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testMessageStackTrace.d.ts', + }, + testObserver: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', + }, + testRelatedCode: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testRelatedCode.d.ts', + }, + textSearchCompleteNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchCompleteNew.d.ts', + }, + textSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', + }, + textSearchProviderNew: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProviderNew.d.ts', + }, + timeline: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', + }, + tokenInformation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', + }, + treeViewActiveItem: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', + }, + treeViewMarkdownMessage: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts', + }, + treeViewReveal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.d.ts', + }, + tunnelFactory: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts', + }, + tunnels: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts', + }, + workspaceTrust: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts', + } +}; +export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals); +export type ApiProposalName = keyof typeof _allApiProposals; diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index fec3b6eded6..4dae8d5c88b 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { canceled } from 'vs/base/common/errors'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; -import { Event } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { Promises } from 'vs/base/common/async'; +import { canceled } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WindowUtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter { +export class ExtensionHostStarter extends Disposable implements IDisposable, IExtensionHostStarter { readonly _serviceBrand: undefined; @@ -29,16 +29,18 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter @IWindowsMainService private readonly _windowsMainService: IWindowsMainService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { + super(); // On shutdown: gracefully await extension host shutdowns - this._lifecycleMainService.onWillShutdown(e => { + this._register(this._lifecycleMainService.onWillShutdown(e => { this._shutdown = true; e.join('extHostStarter', this._waitForAllExit(6000)); - }); + })); } - dispose(): void { + override dispose(): void { // Intentionally not killing the extension host processes + super.dispose(); } private _getExtHost(id: string): WindowUtilityProcess { @@ -72,7 +74,8 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter const id = String(++ExtensionHostStarter._lastId); const extHost = new WindowUtilityProcess(this._logService, this._windowsMainService, this._telemetryService, this._lifecycleMainService); this._extHosts.set(id, extHost); - extHost.onExit(({ pid, code, signal }) => { + const disposable = extHost.onExit(({ pid, code, signal }) => { + disposable.dispose(); this._logService.info(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`); setTimeout(() => { extHost.dispose(); @@ -110,6 +113,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter execArgv: opts.execArgv, allowLoadingUnsignedLibraries: true, forceAllocationsToV8Sandbox: true, + respondToAuthRequestsFromMainProcess: true, correlationId: id }); const pid = await Event.toPromise(extHost.onSpawn); diff --git a/src/vs/platform/extensions/test/common/extensionValidator.test.ts b/src/vs/platform/extensions/test/common/extensionValidator.test.ts index 9885ec4e29a..6ac5821e08a 100644 --- a/src/vs/platform/extensions/test/common/extensionValidator.test.ts +++ b/src/vs/platform/extensions/test/common/extensionValidator.test.ts @@ -2,11 +2,15 @@ * 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 assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; +import { areApiProposalsCompatible, INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; suite('Extension Version Validator', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + const productVersion = '2021-05-11T21:54:30.577Z'; test('isValidVersionStr', () => { @@ -419,4 +423,21 @@ suite('Extension Version Validator', () => { }; assert.strictEqual(isValidExtensionVersion('1.44.0', undefined, manifest, false, []), false); }); + + test('areApiProposalsCompatible', () => { + assert.strictEqual(areApiProposalsCompatible([]), true); + assert.strictEqual(areApiProposalsCompatible([], ['hello']), true); + assert.strictEqual(areApiProposalsCompatible([], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '' } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '', version: 1 } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 1 } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal2': { proposal: '' } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], { 'proposal1': { proposal: '' } }), true); + + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 2 } }), false); + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '' } }), false); + }); + }); diff --git a/src/vs/platform/extensions/test/common/extensions.test.ts b/src/vs/platform/extensions/test/common/extensions.test.ts new file mode 100644 index 00000000000..7b81268b347 --- /dev/null +++ b/src/vs/platform/extensions/test/common/extensions.test.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; + +suite('Parsing Enabled Api Proposals', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('parsingEnabledApiProposals', () => { + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@randomstring'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1234'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1234_random'])); + }); + +}); diff --git a/src/vs/platform/externalTerminal/node/externalTerminalService.ts b/src/vs/platform/externalTerminal/node/externalTerminalService.ts index 5086c95a802..9f85b145912 100644 --- a/src/vs/platform/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/platform/externalTerminal/node/externalTerminalService.ts @@ -56,8 +56,9 @@ export class WindowsExternalTerminalService extends ExternalTerminalService impl const cmdArgs = ['/c', 'start', '/wait']; if (exec.indexOf(' ') >= 0) { // The "" argument is the window title. Without this, exec doesn't work when the path - // contains spaces - cmdArgs.push('""'); + // contains spaces. #6590 + // Title is Execution Path. #220129 + cmdArgs.push(exec); } cmdArgs.push(exec); // Add starting directory parameter for Windows Terminal (see #90734) diff --git a/src/vs/platform/files/common/diskFileSystemProvider.ts b/src/vs/platform/files/common/diskFileSystemProvider.ts index 5f5b623201b..8e5d9e45e48 100644 --- a/src/vs/platform/files/common/diskFileSystemProvider.ts +++ b/src/vs/platform/files/common/diskFileSystemProvider.ts @@ -7,6 +7,7 @@ import { insert } from 'vs/base/common/arrays'; import { ThrottledDelayer } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; +import { removeTrailingPathSeparator } from 'vs/base/common/extpath'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { normalize } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; @@ -66,14 +67,21 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen private universalWatcher: AbstractUniversalWatcherClient | undefined; - private readonly universalPathsToWatch: IUniversalWatchRequest[] = []; + private readonly universalWatchRequests: IUniversalWatchRequest[] = []; private readonly universalWatchRequestDelayer = this._register(new ThrottledDelayer(0)); private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable { // Add to list of paths to watch universally - const pathToWatch: IUniversalWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: opts.recursive, correlationId: opts.correlationId }; - const remove = insert(this.universalPathsToWatch, pathToWatch); + const request: IUniversalWatchRequest = { + path: this.toWatchPath(resource), + excludes: opts.excludes, + includes: opts.includes, + recursive: opts.recursive, + filter: opts.filter, + correlationId: opts.correlationId + }; + const remove = insert(this.universalWatchRequests, request); // Trigger update this.refreshUniversalWatchers(); @@ -116,13 +124,13 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen // Adjust for polling const usePolling = this.options?.watcher?.recursive?.usePolling; if (usePolling === true) { - for (const request of this.universalPathsToWatch) { + for (const request of this.universalWatchRequests) { if (isRecursiveWatchRequest(request)) { request.pollingInterval = this.options?.watcher?.recursive?.pollingInterval ?? 5000; } } } else if (Array.isArray(usePolling)) { - for (const request of this.universalPathsToWatch) { + for (const request of this.universalWatchRequests) { if (isRecursiveWatchRequest(request)) { if (usePolling.includes(request.path)) { request.pollingInterval = this.options?.watcher?.recursive?.pollingInterval ?? 5000; @@ -132,7 +140,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen } // Ask to watch the provided paths - return this.universalWatcher.watch(this.universalPathsToWatch); + return this.universalWatcher.watch(this.universalWatchRequests); } protected abstract createUniversalWatcher( @@ -147,14 +155,21 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen private nonRecursiveWatcher: AbstractNonRecursiveWatcherClient | undefined; - private readonly nonRecursivePathsToWatch: INonRecursiveWatchRequest[] = []; + private readonly nonRecursiveWatchRequests: INonRecursiveWatchRequest[] = []; private readonly nonRecursiveWatchRequestDelayer = this._register(new ThrottledDelayer(0)); private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable { // Add to list of paths to watch non-recursively - const pathToWatch: INonRecursiveWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: false, correlationId: opts.correlationId }; - const remove = insert(this.nonRecursivePathsToWatch, pathToWatch); + const request: INonRecursiveWatchRequest = { + path: this.toWatchPath(resource), + excludes: opts.excludes, + includes: opts.includes, + recursive: false, + filter: opts.filter, + correlationId: opts.correlationId + }; + const remove = insert(this.nonRecursiveWatchRequests, request); // Trigger update this.refreshNonRecursiveWatchers(); @@ -195,7 +210,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen } // Ask to watch the provided paths - return this.nonRecursiveWatcher.watch(this.nonRecursivePathsToWatch); + return this.nonRecursiveWatcher.watch(this.nonRecursiveWatchRequests); } protected abstract createNonRecursiveWatcher( @@ -211,10 +226,24 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen this._onDidWatchError.fire(msg.message); } + this.logWatcherMessage(msg); + } + + protected logWatcherMessage(msg: ILogMessage): void { this.logService[msg.type](msg.message); } protected toFilePath(resource: URI): string { return normalize(resource.fsPath); } + + private toWatchPath(resource: URI): string { + const filePath = this.toFilePath(resource); + + // Ensure to have any trailing path separators removed, otherwise + // we may believe the path is not "real" and will convert every + // event back to this form, which is not warranted. + // See also https://github.com/microsoft/vscode/issues/210517 + return removeTrailingPathSeparator(filePath); + } } diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index e8bcce418a8..82d4f9ba0f3 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -519,6 +519,15 @@ export interface IWatchOptionsWithoutCorrelation { * always matched relative to the watched folder. */ includes?: Array; + + /** + * If provided, allows to filter the events that the watcher should consider + * for emitting. If not provided, all events are emitted. + * + * For example, to emit added and updated events, set to: + * `FileChangeFilter.ADDED | FileChangeFilter.UPDATED`. + */ + filter?: FileChangeFilter; } export interface IWatchOptions extends IWatchOptionsWithoutCorrelation { @@ -531,6 +540,12 @@ export interface IWatchOptions extends IWatchOptionsWithoutCorrelation { readonly correlationId?: number; } +export const enum FileChangeFilter { + UPDATED = 1 << 1, + ADDED = 1 << 2, + DELETED = 1 << 3 +} + export interface IWatchOptionsWithCorrelation extends IWatchOptions { readonly correlationId: number; } @@ -1445,7 +1460,7 @@ export interface IGlobPatterns { } export interface IFilesConfiguration { - files: IFilesConfigurationNode; + files?: IFilesConfigurationNode; } export interface IFilesConfigurationNode { @@ -1455,6 +1470,7 @@ export interface IFilesConfigurationNode { watcherInclude: string[]; encoding: string; autoGuessEncoding: boolean; + candidateGuessEncodings: string[]; defaultLanguage: string; trimTrailingWhitespace: boolean; autoSave: string; diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index 3aeba5d80ad..eef16ccfcb0 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -5,11 +5,11 @@ import { Event } from 'vs/base/common/event'; import { GLOBSTAR, IRelativePattern, parse, ParsedPattern } from 'vs/base/common/glob'; -import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isAbsolute } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files'; +import { FileChangeFilter, FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files'; interface IWatchRequest { @@ -41,6 +41,15 @@ interface IWatchRequest { * id. */ readonly correlationId?: number; + + /** + * If provided, allows to filter the events that the watcher should consider + * for emitting. If not provided, all events are emitted. + * + * For example, to emit added and updated events, set to: + * `FileChangeFilter.ADDED | FileChangeFilter.UPDATED`. + */ + readonly filter?: FileChangeFilter; } export interface IWatchRequestWithCorrelation extends IWatchRequest { @@ -79,6 +88,11 @@ export function isRecursiveWatchRequest(request: IWatchRequest): request is IRec export type IUniversalWatchRequest = IRecursiveWatchRequest | INonRecursiveWatchRequest; +export interface IWatcherErrorEvent { + readonly error: string; + readonly request?: IUniversalWatchRequest; +} + export interface IWatcher { /** @@ -97,7 +111,7 @@ export interface IWatcher { * that is unrecoverable. Listeners should restart the * watcher if possible. */ - readonly onDidError: Event; + readonly onDidError: Event; /** * Configures the watcher to watch according to the @@ -122,6 +136,20 @@ export interface IRecursiveWatcher extends IWatcher { watch(requests: IRecursiveWatchRequest[]): Promise; } +export interface IRecursiveWatcherWithSubscribe extends IRecursiveWatcher { + + /** + * Subscribe to file events for the given path. The callback is called + * whenever a file event occurs for the path. If the watcher failed, + * the error parameter is set to `true`. + * + * @returns an `IDisposable` to stop listening to events or `undefined` + * if no events can be watched for the path given the current set of + * recursive watch requests. + */ + subscribe(path: string, callback: (error: true | null, change?: IFileChange) => void): IDisposable | undefined; +} + export interface IRecursiveWatcherOptions { /** @@ -167,8 +195,8 @@ export abstract class AbstractWatcherClient extends Disposable { private readonly onLogMessage: (msg: ILogMessage) => void, private verboseLogging: boolean, private options: { - type: string; - restartOnError: boolean; + readonly type: string; + readonly restartOnError: boolean; } ) { super(); @@ -189,27 +217,53 @@ export abstract class AbstractWatcherClient extends Disposable { // Wire in event handlers disposables.add(this.watcher.onDidChangeFile(changes => this.onFileChanges(changes))); disposables.add(this.watcher.onDidLogMessage(msg => this.onLogMessage(msg))); - disposables.add(this.watcher.onDidError(error => this.onError(error))); + disposables.add(this.watcher.onDidError(e => this.onError(e.error, e.request))); } - protected onError(error: string): void { + protected onError(error: string, failedRequest?: IUniversalWatchRequest): void { - // Restart on error (up to N times, if enabled) - if (this.options.restartOnError) { + // Restart on error (up to N times, if possible) + if (this.canRestart(error, failedRequest)) { if (this.restartCounter < AbstractWatcherClient.MAX_RESTARTS && this.requests) { - this.error(`restarting watcher after error: ${error}`); + this.error(`restarting watcher after unexpected error: ${error}`); this.restart(this.requests); } else { - this.error(`gave up attempting to restart watcher after error: ${error}`); + this.error(`gave up attempting to restart watcher after unexpected error: ${error}`); } } - // Do not attempt to restart if not enabled + // Do not attempt to restart otherwise, report the error else { this.error(error); } } + private canRestart(error: string, failedRequest?: IUniversalWatchRequest): boolean { + if (!this.options.restartOnError) { + return false; // disabled by options + } + + if (failedRequest) { + // do not treat a failing request as a reason to restart the entire + // watcher. it is possible that from a large amount of watch requests + // some fail and we would constantly restart all requests only because + // of that. rather, continue the watcher and leave the failed request + return false; + } + + if ( + error.indexOf('No space left on device') !== -1 || + error.indexOf('EMFILE') !== -1 + ) { + // do not restart when the error indicates that the system is running + // out of handles for file watching. this is not recoverable anyway + // and needs changes to the system before continuing + return false; + } + + return true; + } + private restart(requests: IUniversalWatchRequest[]): void { this.restartCounter++; @@ -414,3 +468,41 @@ class EventCoalescer { }).concat(addOrChangeEvents); } } + +export function isFiltered(event: IFileChange, filter: FileChangeFilter | undefined): boolean { + if (typeof filter === 'number') { + switch (event.type) { + case FileChangeType.ADDED: + return (filter & FileChangeFilter.ADDED) === 0; + case FileChangeType.DELETED: + return (filter & FileChangeFilter.DELETED) === 0; + case FileChangeType.UPDATED: + return (filter & FileChangeFilter.UPDATED) === 0; + } + } + + return false; +} + +export function requestFilterToString(filter: FileChangeFilter | undefined): string { + if (typeof filter === 'number') { + const filters = []; + if (filter & FileChangeFilter.ADDED) { + filters.push('Added'); + } + if (filter & FileChangeFilter.DELETED) { + filters.push('Deleted'); + } + if (filter & FileChangeFilter.UPDATED) { + filters.push('Updated'); + } + + if (filters.length === 0) { + return ''; + } + + return `[${filters.join(', ')}]`; + } + + return ''; +} diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 180aa7e2960..9993c5f82ba 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import { gracefulify } from 'graceful-fs'; +import { Stats, promises } from 'fs'; import { Barrier, retry } from 'vs/base/common/async'; import { ResourceMap } from 'vs/base/common/map'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -24,22 +23,9 @@ import { readFileIntoStream } from 'vs/platform/files/common/io'; import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from 'vs/platform/files/common/watcher'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/common/diskFileSystemProvider'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; import { UniversalWatcherClient } from 'vs/platform/files/node/watcher/watcherClient'; import { NodeJSWatcherClient } from 'vs/platform/files/node/watcher/nodejs/nodejsClient'; -/** - * Enable graceful-fs very early from here to have it enabled - * in all contexts that leverage the disk file system provider. - */ -(() => { - try { - gracefulify(fs); - } catch (error) { - console.error(`Error enabling graceful-fs: ${toErrorMessage(error)}`); - } -})(); - export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, @@ -139,7 +125,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple } } - private toType(entry: fs.Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { + private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { // Signal file type by checking for file / directory, except: // - symbolic links pointing to nonexistent files are FileType.Unknown @@ -217,7 +203,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple const filePath = this.toFilePath(resource); - return await Promises.readFile(filePath); + return await promises.readFile(filePath); } catch (error) { throw this.toFileSystemProviderError(error); } finally { @@ -368,7 +354,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple try { const { stat } = await SymlinkSupport.stat(filePath); if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) { - await Promises.chmod(filePath, stat.mode | 0o200); + await promises.chmod(filePath, stat.mode | 0o200); } } catch (error) { if (error.code !== 'ENOENT') { @@ -387,7 +373,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - await Promises.truncate(filePath, 0); + await promises.truncate(filePath, 0); // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; @@ -609,7 +595,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple async mkdir(resource: URI): Promise { try { - await Promises.mkdir(this.toFilePath(resource)); + await promises.mkdir(this.toFilePath(resource)); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -627,7 +613,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple await Promises.rm(filePath, RimRafMode.MOVE, rmMoveToPath); } else { try { - await Promises.unlink(filePath); + await promises.unlink(filePath); } catch (unlinkError) { // `fs.unlink` will throw when used on directories @@ -645,7 +631,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple } if (isDirectory) { - await Promises.rmdir(filePath); + await promises.rmdir(filePath); } else { throw unlinkError; } @@ -792,10 +778,10 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple locks.add(await this.createResourceLock(to)); if (mkdir) { - await Promises.mkdir(dirname(toFilePath), { recursive: true }); + await promises.mkdir(dirname(toFilePath), { recursive: true }); } - await Promises.copyFile(fromFilePath, toFilePath); + await promises.copyFile(fromFilePath, toFilePath); } catch (error) { if (error.code === 'ENOENT' && !mkdir) { return this.doCloneFile(from, to, true); diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index 9b9c35ae6c3..c5665396da9 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -5,10 +5,11 @@ import { watchFile, unwatchFile, Stats } from 'fs'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { ILogMessage, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, isWatchRequestWithCorrelation } from 'vs/platform/files/common/watcher'; +import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, IWatcherErrorEvent, isWatchRequestWithCorrelation, requestFilterToString } from 'vs/platform/files/common/watcher'; import { Emitter, Event } from 'vs/base/common/event'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; +import { DeferredPromise, ThrottledDelayer } from 'vs/base/common/async'; export abstract class BaseWatcher extends Disposable implements IWatcher { @@ -25,9 +26,14 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { private readonly allCorrelatedWatchRequests = new Map(); private readonly suspendedWatchRequests = this._register(new DisposableMap()); + private readonly suspendedWatchRequestsWithPolling = new Set(); + + private readonly updateWatchersDelayer = this._register(new ThrottledDelayer(this.getUpdateWatchersDelay())); protected readonly suspendedWatchRequestPollingInterval: number = 5007; // node.js default + private joinWatch = new DeferredPromise(); + constructor() { super(); @@ -41,6 +47,11 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { // to experiment with this feature in a controlled way. Monitoring requests // requires us to install polling watchers (via `fs.watchFile()`) and thus // should be used sparingly. + // + // TODO@bpasero revisit this in the future to have a more general approach + // for suspend/resume and drop the `legacyMonitorRequest` in parcel. + // One issue is that we need to be able to uniquely identify a request and + // without correlation that is actually harder... return; } @@ -53,36 +64,58 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { } async watch(requests: IUniversalWatchRequest[]): Promise { - this.allCorrelatedWatchRequests.clear(); - this.allNonCorrelatedWatchRequests.clear(); - - // Figure out correlated vs. non-correlated requests - for (const request of requests) { - if (this.isCorrelated(request)) { - this.allCorrelatedWatchRequests.set(request.correlationId, request); - } else { - this.allNonCorrelatedWatchRequests.add(request); - } + if (!this.joinWatch.isSettled) { + this.joinWatch.complete(); } + this.joinWatch = new DeferredPromise(); - // Remove all suspended correlated watch requests that are no longer watched - for (const [correlationId] of this.suspendedWatchRequests) { - if (!this.allCorrelatedWatchRequests.has(correlationId)) { - this.suspendedWatchRequests.deleteAndDispose(correlationId); + try { + this.allCorrelatedWatchRequests.clear(); + this.allNonCorrelatedWatchRequests.clear(); + + // Figure out correlated vs. non-correlated requests + for (const request of requests) { + if (this.isCorrelated(request)) { + this.allCorrelatedWatchRequests.set(request.correlationId, request); + } else { + this.allNonCorrelatedWatchRequests.add(request); + } } - } - return this.updateWatchers(); + // Remove all suspended correlated watch requests that are no longer watched + for (const [correlationId] of this.suspendedWatchRequests) { + if (!this.allCorrelatedWatchRequests.has(correlationId)) { + this.suspendedWatchRequests.deleteAndDispose(correlationId); + this.suspendedWatchRequestsWithPolling.delete(correlationId); + } + } + + return await this.updateWatchers(false /* not delayed */); + } finally { + this.joinWatch.complete(); + } } - private updateWatchers(): Promise { - return this.doWatch([ + private updateWatchers(delayed: boolean): Promise { + return this.updateWatchersDelayer.trigger(() => this.doWatch([ ...this.allNonCorrelatedWatchRequests, ...Array.from(this.allCorrelatedWatchRequests.values()).filter(request => !this.suspendedWatchRequests.has(request.correlationId)) - ]); + ]), delayed ? this.getUpdateWatchersDelay() : 0); } - private suspendWatchRequest(request: IWatchRequestWithCorrelation): void { + protected getUpdateWatchersDelay(): number { + return 800; + } + + isSuspended(request: IUniversalWatchRequest): 'polling' | boolean { + if (typeof request.correlationId !== 'number') { + return false; + } + + return this.suspendedWatchRequestsWithPolling.has(request.correlationId) ? 'polling' : this.suspendedWatchRequests.has(request.correlationId); + } + + private async suspendWatchRequest(request: IWatchRequestWithCorrelation): Promise { if (this.suspendedWatchRequests.has(request.correlationId)) { return; // already suspended } @@ -90,21 +123,62 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { const disposables = new DisposableStore(); this.suspendedWatchRequests.set(request.correlationId, disposables); + // It is possible that a watch request fails right during watch() + // phase while other requests succeed. To increase the chance of + // reusing another watcher for suspend/resume tracking, we await + // all watch requests having processed. + + await this.joinWatch.p; + + if (disposables.isDisposed) { + return; + } + this.monitorSuspendedWatchRequest(request, disposables); - this.updateWatchers(); + this.updateWatchers(true /* delay this call as we might accumulate many failing watch requests on startup */); } private resumeWatchRequest(request: IWatchRequestWithCorrelation): void { this.suspendedWatchRequests.deleteAndDispose(request.correlationId); + this.suspendedWatchRequestsWithPolling.delete(request.correlationId); - this.updateWatchers(); + this.updateWatchers(false); } - private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore) { - const resource = URI.file(request.path); - const that = this; + private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void { + if (this.doMonitorWithExistingWatcher(request, disposables)) { + this.trace(`reusing an existing recursive watcher to monitor ${request.path}`); + this.suspendedWatchRequestsWithPolling.delete(request.correlationId); + } else { + this.doMonitorWithNodeJS(request, disposables); + this.suspendedWatchRequestsWithPolling.add(request.correlationId); + } + } + private doMonitorWithExistingWatcher(request: IWatchRequestWithCorrelation, disposables: DisposableStore): boolean { + const subscription = this.recursiveWatcher?.subscribe(request.path, (error, change) => { + if (disposables.isDisposed) { + return; // return early if already disposed + } + + if (error) { + this.monitorSuspendedWatchRequest(request, disposables); + } else if (change?.type === FileChangeType.ADDED) { + this.onMonitoredPathAdded(request); + } + }); + + if (subscription) { + disposables.add(subscription); + + return true; + } + + return false; + } + + private doMonitorWithNodeJS(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void { let pathNotFound = false; const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => { @@ -119,15 +193,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { // Watch path created: resume watching request if (!currentPathNotFound && (previousPathNotFound || oldPathNotFound)) { - this.trace(`fs.watchFile() detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`); - - // Emit as event - const event: IFileChange = { resource, type: FileChangeType.ADDED, cId: request.correlationId }; - that._onDidChangeFile.fire([event]); - this.traceEvent(event, request); - - // Resume watching - this.resumeWatchRequest(request); + this.onMonitoredPathAdded(request); } }; @@ -149,28 +215,56 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { })); } + private onMonitoredPathAdded(request: IWatchRequestWithCorrelation) { + this.trace(`detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`); + + // Emit as event + const event: IFileChange = { resource: URI.file(request.path), type: FileChangeType.ADDED, cId: request.correlationId }; + this._onDidChangeFile.fire([event]); + this.traceEvent(event, request); + + // Resume watching + this.resumeWatchRequest(request); + } + private isPathNotFound(stats: Stats): boolean { return stats.ctimeMs === 0 && stats.ino === 0; } async stop(): Promise { this.suspendedWatchRequests.clearAndDisposeAll(); + this.suspendedWatchRequestsWithPolling.clear(); } protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void { - const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`; - this.trace(typeof request.correlationId === 'number' ? `${traceMsg} (correlationId: ${request.correlationId})` : traceMsg); + if (this.verboseLogging) { + const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`; + this.traceWithCorrelation(traceMsg, request); + } + } + + protected traceWithCorrelation(message: string, request: IUniversalWatchRequest): void { + if (this.verboseLogging) { + this.trace(`${message}${typeof request.correlationId === 'number' ? ` <${request.correlationId}> ` : ``}`); + } } protected requestToString(request: IUniversalWatchRequest): string { - return `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : ''}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : ''}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : ''})`; + return `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : ''}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : ''}, filter: ${requestFilterToString(request.filter)}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : ''})`; } protected abstract doWatch(requests: IUniversalWatchRequest[]): Promise; + protected abstract readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined; + protected abstract trace(message: string): void; protected abstract warn(message: string): void; - abstract onDidError: Event; - abstract setVerboseLogging(enabled: boolean): Promise; + abstract onDidError: Event; + + protected verboseLogging = false; + + async setVerboseLogging(enabled: boolean): Promise { + this.verboseLogging = enabled; + } } diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts index 2a662eb7e05..3a2f6446996 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsClient.ts @@ -21,6 +21,6 @@ export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient { } protected override createWatcher(disposables: DisposableStore): INonRecursiveWatcher { - return disposables.add(new NodeJSWatcher()) satisfies INonRecursiveWatcher; + return disposables.add(new NodeJSWatcher(undefined /* no recursive watching support here */)) satisfies INonRecursiveWatcher; } } diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index 197c975a465..16f861668ac 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event'; import { patternsEquals } from 'vs/base/common/glob'; import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher'; import { isLinux } from 'vs/base/common/platform'; -import { INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; +import { INonRecursiveWatchRequest, INonRecursiveWatcher, IRecursiveWatcherWithSubscribe } from 'vs/platform/files/common/watcher'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { isEqual } from 'vs/base/common/extpath'; @@ -28,9 +28,11 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { readonly onDidError = Event.None; - protected readonly watchers = new Set(); + readonly watchers = new Set(); - private verboseLogging = false; + constructor(protected readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined) { + super(); + } protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { @@ -47,7 +49,6 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { } else { requestsToStart.push(request); // start watching } - } // Logging @@ -95,7 +96,7 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { private startWatching(request: INonRecursiveWatchRequest): void { // Start via node.js lib - const instance = new NodeJSFileWatcherLibrary(request, changes => this._onDidChangeFile.fire(changes), () => this._onDidWatchFail.fire(request), msg => this._onDidLogMessage.fire(msg), this.verboseLogging); + const instance = new NodeJSFileWatcherLibrary(request, this.recursiveWatcher, changes => this._onDidChangeFile.fire(changes), () => this._onDidWatchFail.fire(request), msg => this._onDidLogMessage.fire(msg), this.verboseLogging); // Remember as watcher instance const watcher: INodeJSWatcherInstance = { request, instance }; @@ -141,8 +142,8 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { return Array.from(mapCorrelationtoRequests.values()).map(requests => Array.from(requests.values())).flat(); } - async setVerboseLogging(enabled: boolean): Promise { - this.verboseLogging = enabled; + override async setVerboseLogging(enabled: boolean): Promise { + super.setVerboseLogging(enabled); for (const watcher of this.watchers) { watcher.instance.setVerboseLogging(enabled); diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index ba9edc88f0f..eec6a2232c9 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { watch } from 'fs'; +import { watch, promises } from 'fs'; import { RunOnceWorker, ThrottledWorker } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { isEqualOrParent } from 'vs/base/common/extpath'; @@ -16,7 +16,7 @@ import { URI } from 'vs/base/common/uri'; import { realcase } from 'vs/base/node/extpath'; import { Promises } from 'vs/base/node/pfs'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; +import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, isWatchRequestWithCorrelation } from 'vs/platform/files/common/watcher'; export class NodeJSFileWatcherLibrary extends Disposable { @@ -51,13 +51,21 @@ export class NodeJSFileWatcherLibrary extends Disposable { private readonly excludes = parseWatcherPatterns(this.request.path, this.request.excludes); private readonly includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined; + private readonly filter = isWatchRequestWithCorrelation(this.request) ? this.request.filter : undefined; // TODO@bpasero filtering for now is only enabled when correlating because watchers are otherwise potentially reused private readonly cts = new CancellationTokenSource(); readonly ready = this.watch(); + private _isReusingRecursiveWatcher = false; + get isReusingRecursiveWatcher(): boolean { return this._isReusingRecursiveWatcher; } + + private didFail = false; + get failed(): boolean { return this.didFail; } + constructor( private readonly request: INonRecursiveWatchRequest, + private readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined, private readonly onDidFilesChange: (changes: IFileChange[]) => void, private readonly onDidWatchFail?: () => void, private readonly onLogMessage?: (msg: ILogMessage) => void, @@ -74,7 +82,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { return; } - const stat = await Promises.stat(realPath); + const stat = await promises.stat(realPath); if (this.cts.token.isCancellationRequested) { return; @@ -88,10 +96,16 @@ export class NodeJSFileWatcherLibrary extends Disposable { this.trace(`ignoring a path for watching who's stat info failed to resolve: ${this.request.path} (error: ${error})`); } - this.onDidWatchFail?.(); + this.notifyWatchFailed(); } } + private notifyWatchFailed(): void { + this.didFail = true; + + this.onDidWatchFail?.(); + } + private async normalizePath(request: INonRecursiveWatchRequest): Promise { let realPath = request.path; @@ -117,41 +131,100 @@ export class NodeJSFileWatcherLibrary extends Disposable { return realPath; } - private async doWatch(path: string, isDirectory: boolean): Promise { + private async doWatch(realPath: string, isDirectory: boolean): Promise { + const disposables = new DisposableStore(); + + if (this.doWatchWithExistingWatcher(realPath, isDirectory, disposables)) { + this.trace(`reusing an existing recursive watcher for ${this.request.path}`); + this._isReusingRecursiveWatcher = true; + } else { + this._isReusingRecursiveWatcher = false; + await this.doWatchWithNodeJS(realPath, isDirectory, disposables); + } + + return disposables; + } + + private doWatchWithExistingWatcher(realPath: string, isDirectory: boolean, disposables: DisposableStore): boolean { + if (isDirectory) { + // TODO@bpasero recursive watcher re-use is currently not enabled + // for when folders are watched. this is because the dispatching + // in the recursive watcher for non-recurive requests is optimized + // for file changes where we really only match on the exact path + // and not child paths. + return false; + } + + const resource = URI.file(this.request.path); + const subscription = this.recursiveWatcher?.subscribe(this.request.path, async (error, change) => { + if (disposables.isDisposed) { + return; // return early if already disposed + } + + if (error) { + const watchDisposable = await this.doWatch(realPath, isDirectory); + if (!disposables.isDisposed) { + disposables.add(watchDisposable); + } else { + watchDisposable.dispose(); + } + } else if (change) { + if (typeof change.cId === 'number' || typeof this.request.correlationId === 'number') { + // Re-emit this change with the correlation id of the request + // so that the client can correlate the event with the request + // properly. Without correlation, we do not have to do that + // because the event will appear on the global listener already. + this.onFileChange({ resource, type: change.type, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); + } + } + }); + + if (subscription) { + disposables.add(subscription); + + return true; + } + + return false; + } + + private async doWatchWithNodeJS(realPath: string, isDirectory: boolean, disposables: DisposableStore): Promise { // macOS: watching samba shares can crash VSCode so we do // a simple check for the file path pointing to /Volumes // (https://github.com/microsoft/vscode/issues/106879) // TODO@electron this needs a revisit when the crash is // fixed or mitigated upstream. - if (isMacintosh && isEqualOrParent(path, '/Volumes/', true)) { - this.error(`Refusing to watch ${path} for changes using fs.watch() for possibly being a network share where watching is unreliable and unstable.`); + if (isMacintosh && isEqualOrParent(realPath, '/Volumes/', true)) { + this.error(`Refusing to watch ${realPath} for changes using fs.watch() for possibly being a network share where watching is unreliable and unstable.`); - return Disposable.None; + return; } const cts = new CancellationTokenSource(this.cts.token); + disposables.add(toDisposable(() => cts.dispose(true))); - const disposables = new DisposableStore(); + const watcherDisposables = new DisposableStore(); // we need a separate disposable store because we re-create the watcher from within in some cases + disposables.add(watcherDisposables); try { const requestResource = URI.file(this.request.path); - const pathBasename = basename(path); + const pathBasename = basename(realPath); // Creating watcher can fail with an exception - const watcher = watch(path); - disposables.add(toDisposable(() => { + const watcher = watch(realPath); + watcherDisposables.add(toDisposable(() => { watcher.removeAllListeners(); watcher.close(); })); - this.trace(`Started watching: '${path}'`); + this.trace(`Started watching: '${realPath}'`); // Folder: resolve children to emit proper events const folderChildren = new Set(); if (isDirectory) { try { - for (const child of await Promises.readdir(path)) { + for (const child of await Promises.readdir(realPath)) { folderChildren.add(child); } } catch (error) { @@ -159,8 +232,12 @@ export class NodeJSFileWatcherLibrary extends Disposable { } } + if (cts.token.isCancellationRequested) { + return; + } + const mapPathToStatDisposable = new Map(); - disposables.add(toDisposable(() => { + watcherDisposables.add(toDisposable(() => { for (const [, disposable] of mapPathToStatDisposable) { disposable.dispose(); } @@ -168,9 +245,13 @@ export class NodeJSFileWatcherLibrary extends Disposable { })); watcher.on('error', (code: number, signal: string) => { - this.error(`Failed to watch ${path} for changes using fs.watch() (${code}, ${signal})`); + if (cts.token.isCancellationRequested) { + return; + } - this.onDidWatchFail?.(); + this.error(`Failed to watch ${realPath} for changes using fs.watch() (${code}, ${signal})`); + + this.notifyWatchFailed(); }); watcher.on('change', (type, raw) => { @@ -178,7 +259,9 @@ export class NodeJSFileWatcherLibrary extends Disposable { return; // ignore if already disposed } - this.trace(`[raw] ["${type}"] ${raw}`); + if (this.verboseLogging) { + this.traceWithCorrelation(`[raw] ["${type}"] ${raw}`); + } // Normalize file name let changedFileName = ''; @@ -228,17 +311,21 @@ export class NodeJSFileWatcherLibrary extends Disposable { // file watching specifically we want to handle // the atomic-write cases where the file is being // deleted and recreated with different contents. - if (changedFileName === pathBasename && !await Promises.exists(path)) { + if (changedFileName === pathBasename && !await Promises.exists(realPath)) { this.onWatchedPathDeleted(requestResource); return; } + if (cts.token.isCancellationRequested) { + return; + } + // In order to properly detect renames on a case-insensitive // file system, we need to use `existsChildStrictCase` helper // because otherwise we would wrongly assume a file exists // when it was renamed to same name but different case. - const fileExists = await this.existsChildStrictCase(join(path, changedFileName)); + const fileExists = await this.existsChildStrictCase(join(realPath, changedFileName)); if (cts.token.isCancellationRequested) { return; // ignore if disposed by now @@ -310,7 +397,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { // because the watcher is disposed then. const timeoutHandle = setTimeout(async () => { - const fileExists = await Promises.exists(path); + const fileExists = await Promises.exists(realPath); if (cts.token.isCancellationRequested) { return; // ignore if disposed by now @@ -320,7 +407,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { if (fileExists) { this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); - disposables.add(await this.doWatch(path, false)); + watcherDisposables.add(await this.doWatch(realPath, false)); } // File seems to be really gone, so emit a deleted and failed event @@ -331,8 +418,8 @@ export class NodeJSFileWatcherLibrary extends Disposable { // Very important to dispose the watcher which now points to a stale inode // and wire in a new disposable that tracks our timeout that is installed - disposables.clear(); - disposables.add(toDisposable(() => clearTimeout(timeoutHandle))); + watcherDisposables.clear(); + watcherDisposables.add(toDisposable(() => clearTimeout(timeoutHandle))); } // File changed @@ -343,16 +430,11 @@ export class NodeJSFileWatcherLibrary extends Disposable { }); } catch (error) { if (!cts.token.isCancellationRequested) { - this.error(`Failed to watch ${path} for changes using fs.watch() (${error.toString()})`); + this.error(`Failed to watch ${realPath} for changes using fs.watch() (${error.toString()})`); } - this.onDidWatchFail?.(); + this.notifyWatchFailed(); } - - return toDisposable(() => { - cts.dispose(true); - disposables.dispose(); - }); } private onWatchedPathDeleted(resource: URI): void { @@ -362,7 +444,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { this.onFileChange({ resource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); this.fileChangesAggregator.flush(); - this.onDidWatchFail?.(); + this.notifyWatchFailed(); } private onFileChange(event: IFileChange, skipIncludeExcludeChecks = false): void { @@ -372,17 +454,17 @@ export class NodeJSFileWatcherLibrary extends Disposable { // Logging if (this.verboseLogging) { - this.trace(`${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`); + this.traceWithCorrelation(`${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`); } // Add to aggregator unless excluded or not included (not if explicitly disabled) if (!skipIncludeExcludeChecks && this.excludes.some(exclude => exclude(event.resource.fsPath))) { if (this.verboseLogging) { - this.trace(` >> ignored (excluded) ${event.resource.fsPath}`); + this.traceWithCorrelation(` >> ignored (excluded) ${event.resource.fsPath}`); } } else if (!skipIncludeExcludeChecks && this.includes && this.includes.length > 0 && !this.includes.some(include => include(event.resource.fsPath))) { if (this.verboseLogging) { - this.trace(` >> ignored (not included) ${event.resource.fsPath}`); + this.traceWithCorrelation(` >> ignored (not included) ${event.resource.fsPath}`); } } else { this.fileChangesAggregator.work(event); @@ -393,25 +475,41 @@ export class NodeJSFileWatcherLibrary extends Disposable { // Coalesce events: merge events of same kind const coalescedFileChanges = coalesceEvents(fileChanges); - if (coalescedFileChanges.length > 0) { - // Logging - if (this.verboseLogging) { - for (const event of coalescedFileChanges) { - this.trace(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`); + // Filter events: based on request filter property + const filteredEvents: IFileChange[] = []; + for (const event of coalescedFileChanges) { + if (isFiltered(event, this.filter)) { + if (this.verboseLogging) { + this.traceWithCorrelation(` >> ignored (filtered) ${event.resource.fsPath}`); } + + continue; } - // Broadcast to clients via throttled emitter - const worked = this.throttledFileChangesEmitter.work(coalescedFileChanges); + filteredEvents.push(event); + } - // Logging - if (!worked) { - this.warn(`started ignoring events due to too many file change events at once (incoming: ${coalescedFileChanges.length}, most recent change: ${coalescedFileChanges[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); - } else { - if (this.throttledFileChangesEmitter.pending > 0) { - this.trace(`started throttling events due to large amount of file change events at once (pending: ${this.throttledFileChangesEmitter.pending}, most recent change: ${coalescedFileChanges[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); - } + if (filteredEvents.length === 0) { + return; + } + + // Logging + if (this.verboseLogging) { + for (const event of filteredEvents) { + this.traceWithCorrelation(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`); + } + } + + // Broadcast to clients via throttled emitter + const worked = this.throttledFileChangesEmitter.work(filteredEvents); + + // Logging + if (!worked) { + this.warn(`started ignoring events due to too many file change events at once (incoming: ${filteredEvents.length}, most recent change: ${filteredEvents[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); + } else { + if (this.throttledFileChangesEmitter.pending > 0) { + this.trace(`started throttling events due to large amount of file change events at once (pending: ${this.throttledFileChangesEmitter.pending}, most recent change: ${filteredEvents[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`); } } } @@ -455,6 +553,12 @@ export class NodeJSFileWatcherLibrary extends Disposable { } } + private traceWithCorrelation(message: string): void { + if (!this.cts.token.isCancellationRequested && this.verboseLogging) { + this.trace(`${message}${typeof this.request.correlationId === 'number' ? ` <${this.request.correlationId}> ` : ``}`); + } + } + override dispose(): void { this.cts.dispose(true); @@ -476,7 +580,7 @@ export async function watchFileContents(path: string, onData: (chunk: Uint8Array let isReading = false; const request: INonRecursiveWatchRequest = { path, excludes: [], recursive: false }; - const watcher = new NodeJSFileWatcherLibrary(request, changes => { + const watcher = new NodeJSFileWatcherLibrary(request, undefined, changes => { (async () => { for (const { type } of changes) { if (type === FileChangeType.UPDATED) { diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 3ba2370fbe2..46d213c12e8 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -10,9 +10,9 @@ import { URI } from 'vs/base/common/uri'; import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { Emitter } from 'vs/base/common/event'; -import { randomPath, isEqual } from 'vs/base/common/extpath'; -import { GLOBSTAR, ParsedPattern, patternsEquals } from 'vs/base/common/glob'; +import { Emitter, Event } from 'vs/base/common/event'; +import { randomPath, isEqual, isEqualOrParent } from 'vs/base/common/extpath'; +import { GLOBSTAR, patternsEquals } from 'vs/base/common/glob'; import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -21,44 +21,121 @@ import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; +import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from 'vs/platform/files/common/watcher'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -export interface IParcelWatcherInstance { +export class ParcelWatcherInstance extends Disposable { - /** - * Signals when the watcher is ready to watch. - */ - readonly ready: Promise; + private readonly _onDidStop = this._register(new Emitter<{ joinRestart?: Promise }>()); + readonly onDidStop = this._onDidStop.event; - /** - * The watch request associated to the watcher. - */ - readonly request: IRecursiveWatchRequest; + private readonly _onDidFail = this._register(new Emitter()); + readonly onDidFail = this._onDidFail.event; - /** - * How often this watcher has been restarted in case of an unexpected - * shutdown. - */ - readonly restarts: number; + private didFail = false; + get failed(): boolean { return this.didFail; } - /** - * The cancellation token associated with the lifecycle of the watcher. - */ - readonly token: CancellationToken; + private didStop = false; + get stopped(): boolean { return this.didStop; } - /** - * An event aggregator to coalesce events and reduce duplicates. - */ - readonly worker: RunOnceWorker; + private readonly includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined; + private readonly excludes = this.request.excludes ? parseWatcherPatterns(this.request.path, this.request.excludes) : undefined; - /** - * Stops and disposes the watcher. This operation is async to await - * unsubscribe call in Parcel. - */ - stop(): Promise; + private readonly subscriptions = new Map void>>(); + + constructor( + /** + * Signals when the watcher is ready to watch. + */ + readonly ready: Promise, + readonly request: IRecursiveWatchRequest, + /** + * How often this watcher has been restarted in case of an unexpected + * shutdown. + */ + readonly restarts: number, + /** + * The cancellation token associated with the lifecycle of the watcher. + */ + readonly token: CancellationToken, + /** + * An event aggregator to coalesce events and reduce duplicates. + */ + readonly worker: RunOnceWorker, + private readonly stopFn: () => Promise + ) { + super(); + + this._register(toDisposable(() => this.subscriptions.clear())); + } + + subscribe(path: string, callback: (change: IFileChange) => void): IDisposable { + path = URI.file(path).fsPath; // make sure to store the path in `fsPath` form to match it with events later + + let subscriptions = this.subscriptions.get(path); + if (!subscriptions) { + subscriptions = new Set(); + this.subscriptions.set(path, subscriptions); + } + + subscriptions.add(callback); + + return toDisposable(() => { + const subscriptions = this.subscriptions.get(path); + if (subscriptions) { + subscriptions.delete(callback); + + if (subscriptions.size === 0) { + this.subscriptions.delete(path); + } + } + }); + } + + get subscriptionsCount(): number { + return this.subscriptions.size; + } + + notifyFileChange(path: string, change: IFileChange): void { + const subscriptions = this.subscriptions.get(path); + if (subscriptions) { + for (const subscription of subscriptions) { + subscription(change); + } + } + } + + notifyWatchFailed(): void { + this.didFail = true; + + this._onDidFail.fire(); + } + + include(path: string): boolean { + if (!this.includes || this.includes.length === 0) { + return true; // no specific includes defined, include all + } + + return this.includes.some(include => include(path)); + } + + exclude(path: string): boolean { + return Boolean(this.excludes?.some(exclude => exclude(path))); + } + + async stop(joinRestart: Promise | undefined): Promise { + this.didStop = true; + + try { + await this.stopFn(); + } finally { + this._onDidStop.fire({ joinRestart }); + this.dispose(); + } + } } -export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { +export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithSubscribe { private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map( [ @@ -70,10 +147,10 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; - private readonly _onDidError = this._register(new Emitter()); + private readonly _onDidError = this._register(new Emitter()); readonly onDidError = this._onDidError.event; - protected readonly watchers = new Set(); + readonly watchers = new Set(); // A delay for collecting file changes from Parcel // before collecting them for coalescing and emitting. @@ -98,7 +175,6 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { events => this._onDidChangeFile.fire(events) )); - private verboseLogging = false; private enospcErrorLogged = false; constructor() { @@ -150,16 +226,16 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { if (request.pollingInterval) { this.startPolling(request, request.pollingInterval); } else { - this.startWatching(request); + await this.startWatching(request); } } } - private findWatcher(request: IRecursiveWatchRequest): IParcelWatcherInstance | undefined { + private findWatcher(request: IRecursiveWatchRequest): ParcelWatcherInstance | undefined { for (const watcher of this.watchers) { // Requests or watchers with correlation always match on that - if (typeof request.correlationId === 'number' || typeof watcher.request.correlationId === 'number') { + if (this.isCorrelated(request) || this.isCorrelated(watcher.request)) { if (watcher.request.correlationId === request.correlationId) { return watcher; } @@ -184,13 +260,13 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { const snapshotFile = randomPath(tmpdir(), 'vscode-watcher-snapshot'); // Remember as watcher instance - const watcher: IParcelWatcherInstance = { + const watcher: ParcelWatcherInstance = new ParcelWatcherInstance( + instance.p, request, - ready: instance.p, restarts, - token: cts.token, - worker: new RunOnceWorker(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY), - stop: async () => { + cts.token, + new RunOnceWorker(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY), + async () => { cts.dispose(true); watcher.worker.flush(); @@ -199,15 +275,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { pollingWatcher.dispose(); unlinkSync(snapshotFile); } - }; + ); this.watchers.add(watcher); // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - // Warm up include patterns for usage - const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined; - this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`); let counter = 0; @@ -228,7 +301,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { } // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, includePatterns, realPathDiffers, realPathLength); + this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); } // Store a snapshot of files to the snapshot file @@ -249,19 +322,19 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { pollingWatcher.schedule(0); } - private startWatching(request: IRecursiveWatchRequest, restarts = 0): void { + private async startWatching(request: IRecursiveWatchRequest, restarts = 0): Promise { const cts = new CancellationTokenSource(); const instance = new DeferredPromise(); // Remember as watcher instance - const watcher: IParcelWatcherInstance = { + const watcher: ParcelWatcherInstance = new ParcelWatcherInstance( + instance.p, request, - ready: instance.p, restarts, - token: cts.token, - worker: new RunOnceWorker(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY), - stop: async () => { + cts.token, + new RunOnceWorker(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY), + async () => { cts.dispose(true); watcher.worker.flush(); @@ -270,47 +343,47 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { const watcherInstance = await instance.p; await watcherInstance?.unsubscribe(); } - }; + ); this.watchers.add(watcher); // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - // Warm up include patterns for usage - const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined; + try { + const parcelWatcherInstance = await parcelWatcher.subscribe(realPath, (error, parcelEvents) => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } - parcelWatcher.subscribe(realPath, (error, parcelEvents) => { - if (watcher.token.isCancellationRequested) { - return; // return early when disposed - } + // In any case of an error, treat this like a unhandled exception + // that might require the watcher to restart. We do not really know + // the state of parcel at this point and as such will try to restart + // up to our maximum of restarts. + if (error) { + this.onUnexpectedError(error, request); + } - // In any case of an error, treat this like a unhandled exception - // that might require the watcher to restart. We do not really know - // the state of parcel at this point and as such will try to restart - // up to our maximum of restarts. - if (error) { - this.onUnexpectedError(error, watcher); - } + // Handle & emit events + this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); + }, { + backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, + ignore: watcher.request.excludes + }); - // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, includePatterns, realPathDiffers, realPathLength); - }, { - backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, - ignore: watcher.request.excludes - }).then(parcelWatcher => { this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}'`); - instance.complete(parcelWatcher); - }).catch(error => { - this.onUnexpectedError(error, watcher); + instance.complete(parcelWatcherInstance); + } catch (error) { + this.onUnexpectedError(error, request); instance.complete(undefined); + watcher.notifyWatchFailed(); this._onDidWatchFail.fire(request); - }); + } } - private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: IParcelWatcherInstance, includes: ParsedPattern[] | undefined, realPathDiffers: boolean, realPathLength: number): void { + private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: ParcelWatcherInstance, realPathDiffers: boolean, realPathLength: number): void { if (parcelEvents.length === 0) { return; } @@ -321,7 +394,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength); // Check for includes - const includedEvents = this.handleIncludes(watcher, parcelEvents, includes); + const includedEvents = this.handleIncludes(watcher, parcelEvents); // Add to event aggregator for later processing for (const includedEvent of includedEvents) { @@ -329,19 +402,19 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { } } - private handleIncludes(watcher: IParcelWatcherInstance, parcelEvents: parcelWatcher.Event[], includes: ParsedPattern[] | undefined): IFileChange[] { + private handleIncludes(watcher: ParcelWatcherInstance, parcelEvents: parcelWatcher.Event[]): IFileChange[] { const events: IFileChange[] = []; for (const { path, type: parcelEventType } of parcelEvents) { const type = ParcelWatcher.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!; if (this.verboseLogging) { - this.trace(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`); + this.traceWithCorrelation(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`, watcher.request); } // Apply include filter if any - if (includes && includes.length > 0 && !includes.some(include => include(path))) { + if (!watcher.include(path)) { if (this.verboseLogging) { - this.trace(` >> ignored (not included) ${path}`); + this.traceWithCorrelation(` >> ignored (not included) ${path}`, watcher.request); } } else { events.push({ type, resource: URI.file(path), cId: watcher.request.correlationId }); @@ -351,7 +424,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { return events; } - private handleParcelEvents(parcelEvents: IFileChange[], watcher: IParcelWatcherInstance): void { + private handleParcelEvents(parcelEvents: IFileChange[], watcher: ParcelWatcherInstance): void { // Coalesce events: merge events of same kind const coalescedEvents = coalesceEvents(parcelEvents); @@ -368,18 +441,11 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { } } - private emitEvents(events: IFileChange[], watcher: IParcelWatcherInstance): void { + private emitEvents(events: IFileChange[], watcher: ParcelWatcherInstance): void { if (events.length === 0) { return; } - // Logging - if (this.verboseLogging) { - for (const event of events) { - this.traceEvent(event, watcher.request); - } - } - // Broadcast to clients via throttler const worked = this.throttledFileChangesEmitter.work(events); @@ -446,15 +512,22 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { } } - private filterEvents(events: IFileChange[], watcher: IParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } { + private filterEvents(events: IFileChange[], watcher: ParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } { const filteredEvents: IFileChange[] = []; let rootDeleted = false; + const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // TODO@bpasero filtering for now is only enabled when correlating because watchers are otherwise potentially reused for (const event of events) { + + // Emit to instance subscriptions if any before filtering + if (watcher.subscriptionsCount > 0) { + watcher.notifyFileChange(event.resource.fsPath, event); + } + + // Filtering rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux); - - if (rootDeleted && !this.isCorrelated(watcher.request)) { - + if ( + isFiltered(event, filter) || // Explicitly exclude changes to root if we have any // to avoid VS Code closing all opened editors which // can happen e.g. in case of network connectivity @@ -465,39 +538,46 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { // really do not want to skip over file events any // more, so we only ignore this event for non-correlated // watch requests. + (rootDeleted && !this.isCorrelated(watcher.request)) + ) { + if (this.verboseLogging) { + this.traceWithCorrelation(` >> ignored (filtered) ${event.resource.fsPath}`, watcher.request); + } continue; } + // Logging + this.traceEvent(event, watcher.request); + filteredEvents.push(event); } return { events: filteredEvents, rootDeleted }; } - private onWatchedPathDeleted(watcher: IParcelWatcherInstance): void { + private onWatchedPathDeleted(watcher: ParcelWatcherInstance): void { this.warn('Watcher shutdown because watched path got deleted', watcher); - this._onDidWatchFail.fire(watcher.request); - - // Do monitoring of the request path parent unless this request - // can be handled via suspend/resume in the super class - // - // TODO@bpasero we should remove this logic in favor of the - // support in the super class so that we have 1 consistent - // solution for handling this. - + let legacyMonitored = false; if (!this.isCorrelated(watcher.request)) { - this.legacyMonitorRequest(watcher); + // Do monitoring of the request path parent unless this request + // can be handled via suspend/resume in the super class + legacyMonitored = this.legacyMonitorRequest(watcher); + } + + if (!legacyMonitored) { + watcher.notifyWatchFailed(); + this._onDidWatchFail.fire(watcher.request); } } - private legacyMonitorRequest(watcher: IParcelWatcherInstance): void { + private legacyMonitorRequest(watcher: ParcelWatcherInstance): boolean { const parentPath = dirname(watcher.request.path); if (existsSync(parentPath)) { this.trace('Trying to watch on the parent path to restart the watcher...', watcher); - const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, changes => { + const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, undefined, changes => { if (watcher.token.isCancellationRequested) { return; // return early when disposed } @@ -522,10 +602,14 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { // Make sure to stop watching when the watcher is disposed watcher.token.onCancellationRequested(() => nodeWatcher.dispose()); + + return true; } + + return false; } - private onUnexpectedError(error: unknown, watcher?: IParcelWatcherInstance): void { + private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void { const msg = toErrorMessage(error); // Specially handle ENOSPC errors that can happen when @@ -535,7 +619,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { // See https://github.com/microsoft/vscode/issues/7950 if (msg.indexOf('No space left on device') !== -1) { if (!this.enospcErrorLogged) { - this.error('Inotify limit reached (ENOSPC)', watcher); + this.error('Inotify limit reached (ENOSPC)', request); this.enospcErrorLogged = true; } @@ -545,9 +629,9 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { // restart the watcher as a result to get into healthy // state again if possible and if not attempted too much else { - this.error(`Unexpected error: ${msg} (EUNKNOWN)`, watcher); + this.error(`Unexpected error: ${msg} (EUNKNOWN)`, request); - this._onDidError.fire(msg); + this._onDidError.fire({ request, error: msg }); } } @@ -559,7 +643,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { } } - protected restartWatching(watcher: IParcelWatcherInstance, delay = 800): void { + protected restartWatching(watcher: ParcelWatcherInstance, delay = 800): void { // Restart watcher delayed to accomodate for // changes on disk that have triggered the @@ -569,15 +653,21 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { return; // return early when disposed } - // Await the watcher having stopped, as this is - // needed to properly re-watch the same path - await this.stopWatching(watcher); + const restartPromise = new DeferredPromise(); + try { - // Start watcher again counting the restarts - if (watcher.request.pollingInterval) { - this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); - } else { - this.startWatching(watcher.request, watcher.restarts + 1); + // Await the watcher having stopped, as this is + // needed to properly re-watch the same path + await this.stopWatching(watcher, restartPromise.p); + + // Start watcher again counting the restarts + if (watcher.request.pollingInterval) { + this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); + } else { + await this.startWatching(watcher.request, watcher.restarts + 1); + } + } finally { + restartPromise.complete(); } }, delay); @@ -585,15 +675,15 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { watcher.token.onCancellationRequested(() => scheduler.dispose()); } - private async stopWatching(watcher: IParcelWatcherInstance): Promise { + private async stopWatching(watcher: ParcelWatcherInstance, joinRestart?: Promise): Promise { this.trace(`stopping file watcher`, watcher); this.watchers.delete(watcher); try { - await watcher.stop(); + await watcher.stop(joinRestart); } catch (error) { - this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher); + this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher.request); } } @@ -694,25 +784,59 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher { return true; } - async setVerboseLogging(enabled: boolean): Promise { - this.verboseLogging = enabled; + subscribe(path: string, callback: (error: true | null, change?: IFileChange) => void): IDisposable | undefined { + for (const watcher of this.watchers) { + if (watcher.failed) { + continue; // watcher has already failed + } + + if (!isEqualOrParent(path, watcher.request.path, !isLinux)) { + continue; // watcher does not consider this path + } + + if ( + watcher.exclude(path) || + !watcher.include(path) + ) { + continue; // parcel instance does not consider this path + } + + const disposables = new DisposableStore(); + + disposables.add(Event.once(watcher.onDidStop)(async e => { + await e.joinRestart; // if we are restarting, await that so that we can possibly reuse this watcher again + if (disposables.isDisposed) { + return; + } + + callback(true /* error */); + })); + disposables.add(Event.once(watcher.onDidFail)(() => callback(true /* error */))); + disposables.add(watcher.subscribe(path, change => callback(null, change))); + + return disposables; + } + + return undefined; } - protected trace(message: string, watcher?: IParcelWatcherInstance): void { + protected trace(message: string, watcher?: ParcelWatcherInstance): void { if (this.verboseLogging) { - this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) }); + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher?.request) }); } } - protected warn(message: string, watcher?: IParcelWatcherInstance) { - this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) }); + protected warn(message: string, watcher?: ParcelWatcherInstance) { + this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher?.request) }); } - private error(message: string, watcher: IParcelWatcherInstance | undefined) { - this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) }); + private error(message: string, request?: IRecursiveWatchRequest) { + this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, request) }); } - private toMessage(message: string, watcher?: IParcelWatcherInstance): string { - return watcher ? `[File Watcher (parcel)] ${message} (path: ${watcher.request.path})` : `[File Watcher (parcel)] ${message}`; + private toMessage(message: string, request?: IRecursiveWatchRequest): string { + return request ? `[File Watcher (parcel)] ${message} (path: ${request.path})` : `[File Watcher (parcel)] ${message}`; } + + protected get recursiveWatcher() { return this; } } diff --git a/src/vs/platform/files/node/watcher/watcher.ts b/src/vs/platform/files/node/watcher/watcher.ts index d0b563e540c..3ea9fc61213 100644 --- a/src/vs/platform/files/node/watcher/watcher.ts +++ b/src/vs/platform/files/node/watcher/watcher.ts @@ -4,29 +4,61 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher'; -import { Event } from 'vs/base/common/event'; +import { ILogMessage, IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher'; +import { Emitter, Event } from 'vs/base/common/event'; import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; import { Promises } from 'vs/base/common/async'; +import { computeStats } from 'vs/platform/files/node/watcher/watcherStats'; export class UniversalWatcher extends Disposable implements IUniversalWatcher { private readonly recursiveWatcher = this._register(new ParcelWatcher()); - private readonly nonRecursiveWatcher = this._register(new NodeJSWatcher()); + private readonly nonRecursiveWatcher = this._register(new NodeJSWatcher(this.recursiveWatcher)); readonly onDidChangeFile = Event.any(this.recursiveWatcher.onDidChangeFile, this.nonRecursiveWatcher.onDidChangeFile); - readonly onDidLogMessage = Event.any(this.recursiveWatcher.onDidLogMessage, this.nonRecursiveWatcher.onDidLogMessage); readonly onDidError = Event.any(this.recursiveWatcher.onDidError, this.nonRecursiveWatcher.onDidError); + private readonly _onDidLogMessage = this._register(new Emitter()); + readonly onDidLogMessage = Event.any(this._onDidLogMessage.event, this.recursiveWatcher.onDidLogMessage, this.nonRecursiveWatcher.onDidLogMessage); + + private requests: IUniversalWatchRequest[] = []; + async watch(requests: IUniversalWatchRequest[]): Promise { - await Promises.settled([ - this.recursiveWatcher.watch(requests.filter(request => request.recursive)), - this.nonRecursiveWatcher.watch(requests.filter(request => !request.recursive)) - ]); + this.requests = requests; + + // Watch recursively first to give recursive watchers a chance + // to step in for non-recursive watch requests, thus reducing + // watcher duplication. + + let error: Error | undefined; + try { + await this.recursiveWatcher.watch(requests.filter(request => request.recursive)); + } catch (e) { + error = e; + } + + try { + await this.nonRecursiveWatcher.watch(requests.filter(request => !request.recursive)); + } catch (e) { + if (!error) { + error = e; + } + } + + if (error) { + throw error; + } } async setVerboseLogging(enabled: boolean): Promise { + + // Log stats + if (enabled && this.requests.length > 0) { + this._onDidLogMessage.fire({ type: 'trace', message: computeStats(this.requests, this.recursiveWatcher, this.nonRecursiveWatcher) }); + } + + // Forward to watchers await Promises.settled([ this.recursiveWatcher.setVerboseLogging(enabled), this.nonRecursiveWatcher.setVerboseLogging(enabled) diff --git a/src/vs/platform/files/node/watcher/watcherClient.ts b/src/vs/platform/files/node/watcher/watcherClient.ts index 86a89330cd1..f7b39b5f3a1 100644 --- a/src/vs/platform/files/node/watcher/watcherClient.ts +++ b/src/vs/platform/files/node/watcher/watcherClient.ts @@ -40,7 +40,7 @@ export class UniversalWatcherClient extends AbstractUniversalWatcherClient { )); // React on unexpected termination of the watcher process - disposables.add(client.onDidProcessExit(({ code, signal }) => this.onError(`terminated by itself with code ${code}, signal: ${signal}`))); + disposables.add(client.onDidProcessExit(({ code, signal }) => this.onError(`terminated by itself with code ${code}, signal: ${signal} (ETERM)`))); return ProxyChannel.toService(getNextTickChannel(client.getChannel('watcher'))); } diff --git a/src/vs/platform/files/node/watcher/watcherStats.ts b/src/vs/platform/files/node/watcher/watcherStats.ts new file mode 100644 index 00000000000..861e6d24bf8 --- /dev/null +++ b/src/vs/platform/files/node/watcher/watcherStats.ts @@ -0,0 +1,231 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUniversalWatchRequest, requestFilterToString } from 'vs/platform/files/common/watcher'; +import { INodeJSWatcherInstance, NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; +import { ParcelWatcher, ParcelWatcherInstance } from 'vs/platform/files/node/watcher/parcel/parcelWatcher'; + +export function computeStats( + requests: IUniversalWatchRequest[], + recursiveWatcher: ParcelWatcher, + nonRecursiveWatcher: NodeJSWatcher +): string { + const lines: string[] = []; + + const allRecursiveRequests = sortByPathPrefix(requests.filter(request => request.recursive)); + const nonSuspendedRecursiveRequests = allRecursiveRequests.filter(request => recursiveWatcher.isSuspended(request) === false); + const suspendedPollingRecursiveRequests = allRecursiveRequests.filter(request => recursiveWatcher.isSuspended(request) === 'polling'); + const suspendedNonPollingRecursiveRequests = allRecursiveRequests.filter(request => recursiveWatcher.isSuspended(request) === true); + + const recursiveRequestsStatus = computeRequestStatus(allRecursiveRequests, recursiveWatcher); + const recursiveWatcherStatus = computeRecursiveWatchStatus(recursiveWatcher); + + const allNonRecursiveRequests = sortByPathPrefix(requests.filter(request => !request.recursive)); + const nonSuspendedNonRecursiveRequests = allNonRecursiveRequests.filter(request => nonRecursiveWatcher.isSuspended(request) === false); + const suspendedPollingNonRecursiveRequests = allNonRecursiveRequests.filter(request => nonRecursiveWatcher.isSuspended(request) === 'polling'); + const suspendedNonPollingNonRecursiveRequests = allNonRecursiveRequests.filter(request => nonRecursiveWatcher.isSuspended(request) === true); + + const nonRecursiveRequestsStatus = computeRequestStatus(allNonRecursiveRequests, nonRecursiveWatcher); + const nonRecursiveWatcherStatus = computeNonRecursiveWatchStatus(nonRecursiveWatcher); + + lines.push('[Summary]'); + lines.push(`- Recursive Requests: total: ${allRecursiveRequests.length}, suspended: ${recursiveRequestsStatus.suspended}, polling: ${recursiveRequestsStatus.polling}`); + lines.push(`- Non-Recursive Requests: total: ${allNonRecursiveRequests.length}, suspended: ${nonRecursiveRequestsStatus.suspended}, polling: ${nonRecursiveRequestsStatus.polling}`); + lines.push(`- Recursive Watchers: total: ${recursiveWatcher.watchers.size}, active: ${recursiveWatcherStatus.active}, failed: ${recursiveWatcherStatus.failed}, stopped: ${recursiveWatcherStatus.stopped}`); + lines.push(`- Non-Recursive Watchers: total: ${nonRecursiveWatcher.watchers.size}, active: ${nonRecursiveWatcherStatus.active}, failed: ${nonRecursiveWatcherStatus.failed}, reusing: ${nonRecursiveWatcherStatus.reusing}`); + lines.push(`- I/O Handles Impact: total: ${recursiveRequestsStatus.polling + nonRecursiveRequestsStatus.polling + recursiveWatcherStatus.active + nonRecursiveWatcherStatus.active}`); + + lines.push(`\n[Recursive Requests (${allRecursiveRequests.length}, suspended: ${recursiveRequestsStatus.suspended}, polling: ${recursiveRequestsStatus.polling})]:`); + const recursiveRequestLines: string[] = []; + for (const request of [nonSuspendedRecursiveRequests, suspendedPollingRecursiveRequests, suspendedNonPollingRecursiveRequests].flat()) { + fillRequestStats(recursiveRequestLines, request, recursiveWatcher); + } + lines.push(...alignTextColumns(recursiveRequestLines)); + + const recursiveWatcheLines: string[] = []; + fillRecursiveWatcherStats(recursiveWatcheLines, recursiveWatcher); + lines.push(...alignTextColumns(recursiveWatcheLines)); + + lines.push(`\n[Non-Recursive Requests (${allNonRecursiveRequests.length}, suspended: ${nonRecursiveRequestsStatus.suspended}, polling: ${nonRecursiveRequestsStatus.polling})]:`); + const nonRecursiveRequestLines: string[] = []; + for (const request of [nonSuspendedNonRecursiveRequests, suspendedPollingNonRecursiveRequests, suspendedNonPollingNonRecursiveRequests].flat()) { + fillRequestStats(nonRecursiveRequestLines, request, nonRecursiveWatcher); + } + lines.push(...alignTextColumns(nonRecursiveRequestLines)); + + const nonRecursiveWatcheLines: string[] = []; + fillNonRecursiveWatcherStats(nonRecursiveWatcheLines, nonRecursiveWatcher); + lines.push(...alignTextColumns(nonRecursiveWatcheLines)); + + return `\n\n[File Watcher] request stats:\n\n${lines.join('\n')}\n\n`; +} + +function alignTextColumns(lines: string[]) { + let maxLength = 0; + for (const line of lines) { + maxLength = Math.max(maxLength, line.split('\t')[0].length); + } + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split('\t'); + if (parts.length === 2) { + const padding = ' '.repeat(maxLength - parts[0].length); + lines[i] = `${parts[0]}${padding}\t${parts[1]}`; + } + } + + return lines; +} + +function computeRequestStatus(requests: IUniversalWatchRequest[], watcher: ParcelWatcher | NodeJSWatcher): { suspended: number; polling: number } { + let polling = 0; + let suspended = 0; + + for (const request of requests) { + const isSuspended = watcher.isSuspended(request); + if (isSuspended === false) { + continue; + } + + suspended++; + + if (isSuspended === 'polling') { + polling++; + } + } + + return { suspended, polling }; +} + +function computeRecursiveWatchStatus(recursiveWatcher: ParcelWatcher): { active: number; failed: number; stopped: number } { + let active = 0; + let failed = 0; + let stopped = 0; + + for (const watcher of recursiveWatcher.watchers.values()) { + if (!watcher.failed && !watcher.stopped) { + active++; + } + if (watcher.failed) { + failed++; + } + if (watcher.stopped) { + stopped++; + } + } + + return { active, failed, stopped }; +} + +function computeNonRecursiveWatchStatus(nonRecursiveWatcher: NodeJSWatcher): { active: number; failed: number; reusing: number } { + let active = 0; + let failed = 0; + let reusing = 0; + + for (const watcher of nonRecursiveWatcher.watchers) { + if (!watcher.instance.failed && !watcher.instance.isReusingRecursiveWatcher) { + active++; + } + if (watcher.instance.failed) { + failed++; + } + if (watcher.instance.isReusingRecursiveWatcher) { + reusing++; + } + } + + return { active, failed, reusing }; +} + +function sortByPathPrefix(requests: IUniversalWatchRequest[]): IUniversalWatchRequest[]; +function sortByPathPrefix(requests: INodeJSWatcherInstance[]): INodeJSWatcherInstance[]; +function sortByPathPrefix(requests: ParcelWatcherInstance[]): ParcelWatcherInstance[]; +function sortByPathPrefix(requests: IUniversalWatchRequest[] | INodeJSWatcherInstance[] | ParcelWatcherInstance[]): IUniversalWatchRequest[] | INodeJSWatcherInstance[] | ParcelWatcherInstance[] { + requests.sort((r1, r2) => { + const p1 = isUniversalWatchRequest(r1) ? r1.path : r1.request.path; + const p2 = isUniversalWatchRequest(r2) ? r2.path : r2.request.path; + + const minLength = Math.min(p1.length, p2.length); + for (let i = 0; i < minLength; i++) { + if (p1[i] !== p2[i]) { + return (p1[i] < p2[i]) ? -1 : 1; + } + } + + return p1.length - p2.length; + }); + + return requests; +} + +function isUniversalWatchRequest(obj: unknown): obj is IUniversalWatchRequest { + const candidate = obj as IUniversalWatchRequest | undefined; + + return typeof candidate?.path === 'string'; +} + +function fillRequestStats(lines: string[], request: IUniversalWatchRequest, watcher: ParcelWatcher | NodeJSWatcher): void { + const decorations = []; + const suspended = watcher.isSuspended(request); + if (suspended !== false) { + if (suspended === 'polling') { + decorations.push('[SUSPENDED ]'); + } else { + decorations.push('[SUSPENDED ]'); + } + } + + lines.push(` ${request.path}\t${decorations.length > 0 ? decorations.join(' ') + ' ' : ''}(${requestDetailsToString(request)})`); +} + +function requestDetailsToString(request: IUniversalWatchRequest): string { + return `excludes: ${request.excludes.length > 0 ? request.excludes : ''}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : ''}, filter: ${requestFilterToString(request.filter)}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : ''}`; +} + +function fillRecursiveWatcherStats(lines: string[], recursiveWatcher: ParcelWatcher): void { + const watchers = sortByPathPrefix(Array.from(recursiveWatcher.watchers.values())); + + const { active, failed, stopped } = computeRecursiveWatchStatus(recursiveWatcher); + lines.push(`\n[Recursive Watchers (${watchers.length}, active: ${active}, failed: ${failed}, stopped: ${stopped})]:`); + + for (const watcher of watchers) { + const decorations = []; + if (watcher.failed) { + decorations.push('[FAILED]'); + } + if (watcher.stopped) { + decorations.push('[STOPPED]'); + } + if (watcher.subscriptionsCount > 0) { + decorations.push(`[SUBSCRIBED:${watcher.subscriptionsCount}]`); + } + if (watcher.restarts > 0) { + decorations.push(`[RESTARTED:${watcher.restarts}]`); + } + lines.push(` ${watcher.request.path}\t${decorations.length > 0 ? decorations.join(' ') + ' ' : ''}(${requestDetailsToString(watcher.request)})`); + } +} + +function fillNonRecursiveWatcherStats(lines: string[], nonRecursiveWatcher: NodeJSWatcher): void { + const allWatchers = sortByPathPrefix(Array.from(nonRecursiveWatcher.watchers.values())); + const activeWatchers = allWatchers.filter(watcher => !watcher.instance.failed && !watcher.instance.isReusingRecursiveWatcher); + const failedWatchers = allWatchers.filter(watcher => watcher.instance.failed); + const reusingWatchers = allWatchers.filter(watcher => watcher.instance.isReusingRecursiveWatcher); + + const { active, failed, reusing } = computeNonRecursiveWatchStatus(nonRecursiveWatcher); + lines.push(`\n[Non-Recursive Watchers (${allWatchers.length}, active: ${active}, failed: ${failed}, reusing: ${reusing})]:`); + + for (const watcher of [activeWatchers, failedWatchers, reusingWatchers].flat()) { + const decorations = []; + if (watcher.instance.failed) { + decorations.push('[FAILED]'); + } + if (watcher.instance.isReusingRecursiveWatcher) { + decorations.push('[REUSING]'); + } + lines.push(` ${watcher.request.path}\t${decorations.length > 0 ? decorations.join(' ') + ' ' : ''}(${requestDetailsToString(watcher.request)})`); + } +} diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 114e98adefc..166cf549688 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { bufferToReadable, bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts index 9290520ecda..0ee13e7c991 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IndexedDB } from 'vs/base/browser/indexedDB'; import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -189,7 +189,7 @@ flakySuite('IndexedDBFileSystemProvider', function () { assert.strictEqual(value.mtime, undefined); assert.strictEqual(value.ctime, undefined); } else { - assert.ok(!'Unexpected value ' + basename(value.resource)); + assert.fail('Unexpected value ' + basename(value.resource)); } }); }); diff --git a/src/vs/platform/files/test/common/files.test.ts b/src/vs/platform/files/test/common/files.test.ts index 1de7f86398a..245d2222b9d 100644 --- a/src/vs/platform/files/test/common/files.test.ts +++ b/src/vs/platform/files/test/common/files.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isEqual, isEqualOrParent } from 'vs/base/common/extpath'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/files/test/common/watcher.test.ts b/src/vs/platform/files/test/common/watcher.test.ts index 4fadddfc73f..6d7bb7f3b83 100644 --- a/src/vs/platform/files/test/common/watcher.test.ts +++ b/src/vs/platform/files/test/common/watcher.test.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { coalesceEvents, reviveFileChanges, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; +import { FileChangeFilter, FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; +import { coalesceEvents, reviveFileChanges, parseWatcherPatterns, isFiltered } from 'vs/platform/files/common/watcher'; class TestFileWatcher extends Disposable { private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>; @@ -325,5 +325,34 @@ suite('Watcher Events Normalizer', () => { watch.report(raw); }); + test('event type filter', () => { + const resource = URI.file('/users/data/src/related'); + + assert.strictEqual(isFiltered({ resource, type: FileChangeType.ADDED }, undefined), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.UPDATED }, undefined), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.DELETED }, undefined), false); + + assert.strictEqual(isFiltered({ resource, type: FileChangeType.ADDED }, FileChangeFilter.UPDATED), true); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.ADDED }, FileChangeFilter.UPDATED | FileChangeFilter.DELETED), true); + + assert.strictEqual(isFiltered({ resource, type: FileChangeType.ADDED }, FileChangeFilter.ADDED), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.ADDED }, FileChangeFilter.ADDED | FileChangeFilter.UPDATED), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.ADDED }, FileChangeFilter.ADDED | FileChangeFilter.UPDATED | FileChangeFilter.DELETED), false); + + assert.strictEqual(isFiltered({ resource, type: FileChangeType.DELETED }, FileChangeFilter.UPDATED), true); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.DELETED }, FileChangeFilter.UPDATED | FileChangeFilter.ADDED), true); + + assert.strictEqual(isFiltered({ resource, type: FileChangeType.DELETED }, FileChangeFilter.DELETED), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.DELETED }, FileChangeFilter.DELETED | FileChangeFilter.UPDATED), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.DELETED }, FileChangeFilter.ADDED | FileChangeFilter.DELETED | FileChangeFilter.UPDATED), false); + + assert.strictEqual(isFiltered({ resource, type: FileChangeType.UPDATED }, FileChangeFilter.ADDED), true); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.UPDATED }, FileChangeFilter.DELETED | FileChangeFilter.ADDED), true); + + assert.strictEqual(isFiltered({ resource, type: FileChangeType.UPDATED }, FileChangeFilter.UPDATED), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.UPDATED }, FileChangeFilter.DELETED | FileChangeFilter.UPDATED), false); + assert.strictEqual(isFiltered({ resource, type: FileChangeType.UPDATED }, FileChangeFilter.ADDED | FileChangeFilter.DELETED | FileChangeFilter.UPDATED), false); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts index fbf1ea0d870..681e390ec64 100644 --- a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts +++ b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { createReadStream, existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; +import assert from 'assert'; +import { createReadStream, existsSync, readdirSync, readFileSync, statSync, writeFileSync, promises } from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; import { bufferToReadable, bufferToStream, streamToBuffer, streamToBufferReadableStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -273,7 +273,7 @@ flakySuite('Disk File Service', function () { assert.strictEqual(value.mtime, undefined); assert.strictEqual(value.ctime, undefined); } else { - assert.ok(!'Unexpected value ' + basename(value.resource.fsPath)); + assert.fail('Unexpected value ' + basename(value.resource.fsPath)); } }); }); @@ -317,7 +317,7 @@ flakySuite('Disk File Service', function () { assert.ok(value.mtime > 0); assert.ok(value.ctime > 0); } else { - assert.ok(!'Unexpected value ' + basename(value.resource.fsPath)); + assert.fail('Unexpected value ' + basename(value.resource.fsPath)); } }); }); @@ -429,7 +429,7 @@ flakySuite('Disk File Service', function () { test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await Promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); + await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); assert.strictEqual(resolved.children!.length, 4); @@ -439,7 +439,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); + await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); assert.strictEqual(resolved.isDirectory, false); @@ -447,7 +447,7 @@ flakySuite('Disk File Service', function () { }); test('resolve - symbolic link pointing to nonexistent file does not break', async () => { - await Promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); + await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); assert.strictEqual(resolved.isDirectory, true); @@ -530,7 +530,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); - await Promises.symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); const source = await service.resolve(link); @@ -552,7 +552,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to nonexistent file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); - await Promises.symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); @@ -1692,7 +1692,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => { const link = URI.file(join(testDir, 'small.js-link')); - await Promises.symlink(join(testDir, 'small.js'), link.fsPath); + await promises.symlink(join(testDir, 'small.js'), link.fsPath); let error: FileOperationError | undefined = undefined; try { @@ -1833,7 +1833,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('writeFile - atomic writing does not break symlinks', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); + await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const content = 'Updates to the lorem file'; await service.writeFile(link, VSBuffer.fromString(content), { atomic: { postfix: '.vsctmp' } }); @@ -2006,7 +2006,7 @@ flakySuite('Disk File Service', function () { // Here since `close` is not called, all other writes are // waiting on the barrier to release, so doing a readFile // should give us a consistent view of the file contents - assert.strictEqual((await Promises.readFile(resource.fsPath)).toString(), content); + assert.strictEqual((await promises.readFile(resource.fsPath)).toString(), content); } finally { await provider.close(fd); } @@ -2030,10 +2030,10 @@ flakySuite('Disk File Service', function () { try { await provider.write(fd1, 0, VSBuffer.fromString(newContent).buffer, 0, VSBuffer.fromString(newContent).buffer.byteLength); - assert.strictEqual((await Promises.readFile(resource1.fsPath)).toString(), newContent); + assert.strictEqual((await promises.readFile(resource1.fsPath)).toString(), newContent); await provider.write(fd2, 0, VSBuffer.fromString(newContent).buffer, 0, VSBuffer.fromString(newContent).buffer.byteLength); - assert.strictEqual((await Promises.readFile(resource2.fsPath)).toString(), newContent); + assert.strictEqual((await promises.readFile(resource2.fsPath)).toString(), newContent); } finally { await Promise.allSettled([ await provider.close(fd1), @@ -2059,7 +2059,7 @@ flakySuite('Disk File Service', function () { assert.ok(error); // expected because `new-folder` does not exist - await Promises.mkdir(newFolder); + await promises.mkdir(newFolder); const content = readFileSync(URI.file(join(testDir, 'lorem.txt')).fsPath); const newContent = content.toString() + content.toString(); @@ -2069,7 +2069,7 @@ flakySuite('Disk File Service', function () { try { await provider.write(fd, 0, newContentBuffer, 0, newContentBuffer.byteLength); - assert.strictEqual((await Promises.readFile(newResource.fsPath)).toString(), newContent); + assert.strictEqual((await promises.readFile(newResource.fsPath)).toString(), newContent); } finally { await provider.close(fd); } @@ -2291,8 +2291,8 @@ flakySuite('Disk File Service', function () { const content = await service.writeFile(lockedFile, VSBuffer.fromString('Locked File')); assert.strictEqual(content.locked, false); - const stats = await Promises.stat(lockedFile.fsPath); - await Promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); + const stats = await promises.stat(lockedFile.fsPath); + await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); let stat = await service.stat(lockedFile); assert.strictEqual(stat.locked, true); diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 177756f62eb..c53f15426ab 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import assert from 'assert'; import { tmpdir } from 'os'; import { basename, dirname, join } from 'vs/base/common/path'; import { Promises, RimRafMode } from 'vs/base/node/pfs'; -import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { FileChangeType } from 'vs/platform/files/common/files'; -import { INonRecursiveWatchRequest } from 'vs/platform/files/common/watcher'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { FileChangeFilter, FileChangeType } from 'vs/platform/files/common/files'; +import { INonRecursiveWatchRequest, IRecursiveWatcherWithSubscribe } from 'vs/platform/files/common/watcher'; import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { getDriveLetter } from 'vs/base/common/extpath'; @@ -21,13 +23,14 @@ import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; import { Emitter, Event } from 'vs/base/common/event'; +import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.integrationTest'; // this suite has shown flaky runs in Azure pipelines where // tasks would just hang and timeout after a while (not in // mocha but generally). as such they will run only on demand // whenever we update the watcher library. -((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (node.js)', () => { +suite.skip('File Watcher (node.js)', () => { class TestNodeJSWatcher extends NodeJSWatcher { @@ -38,6 +41,10 @@ import { Emitter, Event } from 'vs/base/common/event'; readonly onWatchFail = this._onDidWatchFail.event; + protected override getUpdateWatchersDelay(): number { + return 0; + } + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { await super.doWatch(requests); for (const watcher of this.watchers) { @@ -61,7 +68,20 @@ import { Emitter, Event } from 'vs/base/common/event'; enableLogging(false); setup(async () => { - watcher = new TestNodeJSWatcher(); + await createWatcher(undefined); + + testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath; + + const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath; + + await Promises.copy(sourceDir, testDir, { preserveSymlinks: false }); + }); + + async function createWatcher(accessor: IRecursiveWatcherWithSubscribe | undefined) { + await watcher?.stop(); + watcher?.dispose(); + + watcher = new TestNodeJSWatcher(accessor); watcher?.setVerboseLogging(loggingEnabled); watcher.onDidLogMessage(e => { @@ -75,13 +95,7 @@ import { Emitter, Event } from 'vs/base/common/event'; console.log(`[non-recursive watcher test error] ${e}`); } }); - - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher'); - - const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath; - - await Promises.copy(sourceDir, testDir, { preserveSymlinks: false }); - }); + } teardown(async () => { await watcher.stop(); @@ -128,7 +142,13 @@ import { Emitter, Event } from 'vs/base/common/event'; } test('basics (folder watch)', async function () { - await watcher.watch([{ path: testDir, excludes: [], recursive: false }]); + const request = { path: testDir, excludes: [], recursive: false }; + await watcher.watch([request]); + assert.strictEqual(watcher.isSuspended(request), false); + + const instance = Array.from(watcher.watchers)[0].instance; + assert.strictEqual(instance.isReusingRecursiveWatcher, false); + assert.strictEqual(instance.failed, false); // New file const newFilePath = join(testDir, 'newFile.txt'); @@ -139,7 +159,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // New folder const newFolderPath = join(testDir, 'New Folder'); changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED); - await Promises.mkdir(newFolderPath); + await fs.promises.mkdir(newFolderPath); await changeFuture; // Rename file @@ -201,7 +221,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // Copy file const copiedFilepath = join(testDir, 'copiedFile.txt'); changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.ADDED); - await Promises.copyFile(movedFilepath, copiedFilepath); + await fs.promises.copyFile(movedFilepath, copiedFilepath); await changeFuture; // Copy folder @@ -223,12 +243,12 @@ import { Emitter, Event } from 'vs/base/common/event'; // Delete file changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED); - await Promises.unlink(copiedFilepath); + await fs.promises.unlink(copiedFilepath); await changeFuture; // Delete folder changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED); - await Promises.rmdir(copiedFolderpath); + await fs.promises.rmdir(copiedFolderpath); await changeFuture; watcher.dispose(); @@ -236,7 +256,13 @@ import { Emitter, Event } from 'vs/base/common/event'; test('basics (file watch)', async function () { const filePath = join(testDir, 'lorem.txt'); - await watcher.watch([{ path: filePath, excludes: [], recursive: false }]); + const request = { path: filePath, excludes: [], recursive: false }; + await watcher.watch([request]); + assert.strictEqual(watcher.isSuspended(request), false); + + const instance = Array.from(watcher.watchers)[0].instance; + assert.strictEqual(instance.isReusingRecursiveWatcher, false); + assert.strictEqual(instance.failed, false); // Change file let changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED); @@ -245,7 +271,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED); - await Promises.unlink(filePath); + await fs.promises.unlink(filePath); await changeFuture; // Recreate watcher @@ -265,7 +291,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // Delete + Recreate file const newFilePath = join(testDir, 'lorem.txt'); const changeFuture: Promise = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED); - await Promises.unlink(newFilePath); + await fs.promises.unlink(newFilePath); Promises.writeFile(newFilePath, 'Hello Atomic World'); await changeFuture; }); @@ -277,7 +303,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // Delete + Recreate file const newFilePath = join(filePath); const changeFuture: Promise = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED); - await Promises.unlink(newFilePath); + await fs.promises.unlink(newFilePath); Promises.writeFile(newFilePath, 'Hello Atomic World'); await changeFuture; }); @@ -338,9 +364,9 @@ import { Emitter, Event } from 'vs/base/common/event'; const deleteFuture3: Promise = awaitEvent(watcher, newFilePath3, FileChangeType.DELETED); await Promise.all([ - await Promises.unlink(newFilePath1), - await Promises.unlink(newFilePath2), - await Promises.unlink(newFilePath3) + await fs.promises.unlink(newFilePath1), + await fs.promises.unlink(newFilePath2), + await fs.promises.unlink(newFilePath3) ]); await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3]); @@ -419,7 +445,7 @@ import { Emitter, Event } from 'vs/base/common/event'; (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); - await Promises.symlink(linkTarget, link); + await fs.promises.symlink(linkTarget, link); await watcher.watch([{ path: link, excludes: [], recursive: false }]); @@ -446,14 +472,14 @@ import { Emitter, Event } from 'vs/base/common/event'; // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, correlationId, expectedCount); - await Promises.unlink(await Promises.realpath(filePath)); // support symlinks + await fs.promises.unlink(await Promises.realpath(filePath)); // support symlinks await changeFuture; } (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (file watch)', async function () { const link = join(testDir, 'lorem.txt-linked'); const linkTarget = join(testDir, 'lorem.txt'); - await Promises.symlink(linkTarget, link); + await fs.promises.symlink(linkTarget, link); await watcher.watch([{ path: link, excludes: [], recursive: false }]); @@ -554,17 +580,20 @@ import { Emitter, Event } from 'vs/base/common/event'; await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + const instance = Array.from(watcher.watchers)[0].instance; + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); const changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); - Promises.unlink(filePath); + fs.promises.unlink(filePath); await onDidWatchFail; await changeFuture; + assert.strictEqual(instance.failed, true); }); - (isMacintosh /* macOS: does not seem to report deletes on folders */ ? test.skip : test)('deleting watched path emits watcher fail and delete event when correlated (folder watch)', async function () { + (isMacintosh || isWindows /* macOS: does not seem to report deletes on folders | Windows: reports on('error') event only */ ? test.skip : test)('deleting watched path emits watcher fail and delete event when correlated (folder watch)', async function () { const folderPath = join(testDir, 'deep'); - await watcher.watch([{ path: folderPath, excludes: [], recursive: false }]); + await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1); @@ -577,8 +606,10 @@ import { Emitter, Event } from 'vs/base/common/event'; const filePath = join(testDir, 'not-found.txt'); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 }; + await watcher.watch([request]); await onDidWatchFail; + assert.strictEqual(watcher.isSuspended(request), 'polling'); await basicCrudTest(filePath, undefined, 1, undefined, true); await basicCrudTest(filePath, undefined, 1, undefined, true); @@ -586,11 +617,13 @@ import { Emitter, Event } from 'vs/base/common/event'; test('correlated watch requests support suspend/resume (file, exists in beginning)', async function () { const filePath = join(testDir, 'lorem.txt'); - await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]); + const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 }; + await watcher.watch([request]); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); await basicCrudTest(filePath, true, 1); await onDidWatchFail; + assert.strictEqual(watcher.isSuspended(request), 'polling'); await basicCrudTest(filePath, undefined, 1, undefined, true); }); @@ -599,26 +632,30 @@ import { Emitter, Event } from 'vs/base/common/event'; let onDidWatchFail = Event.toPromise(watcher.onWatchFail); const folderPath = join(testDir, 'not-found'); - await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); + const request = { path: folderPath, excludes: [], recursive: false, correlationId: 1 }; + await watcher.watch([request]); await onDidWatchFail; + assert.strictEqual(watcher.isSuspended(request), 'polling'); let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); let onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await fs.promises.mkdir(folderPath); await changeFuture; await onDidWatch; + assert.strictEqual(watcher.isSuspended(request), false); + const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath, undefined, 1); if (!isMacintosh) { // macOS does not report DELETE events for folders onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await Promises.rmdir(folderPath); + await fs.promises.rmdir(folderPath); await onDidWatchFail; changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await fs.promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -641,7 +678,7 @@ import { Emitter, Event } from 'vs/base/common/event'; const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); const onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await fs.promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -649,4 +686,115 @@ import { Emitter, Event } from 'vs/base/common/event'; await basicCrudTest(filePath, undefined, 1); }); + + test('parcel watcher reused when present for non-recursive file watching (uncorrelated)', function () { + return testParcelWatcherReused(undefined); + }); + + test('parcel watcher reused when present for non-recursive file watching (correlated)', function () { + return testParcelWatcherReused(2); + }); + + function createParcelWatcher() { + const recursiveWatcher = new TestParcelWatcher(); + recursiveWatcher.setVerboseLogging(loggingEnabled); + recursiveWatcher.onDidLogMessage(e => { + if (loggingEnabled) { + console.log(`[recursive watcher test message] ${e.message}`); + } + }); + + recursiveWatcher.onDidError(e => { + if (loggingEnabled) { + console.log(`[recursive watcher test error] ${e.error}`); + } + }); + + return recursiveWatcher; + } + + async function testParcelWatcherReused(correlationId: number | undefined) { + const recursiveWatcher = createParcelWatcher(); + await recursiveWatcher.watch([{ path: testDir, excludes: [], recursive: true, correlationId: 1 }]); + + const recursiveInstance = Array.from(recursiveWatcher.watchers)[0]; + assert.strictEqual(recursiveInstance.subscriptionsCount, 0); + + await createWatcher(recursiveWatcher); + + const filePath = join(testDir, 'deep', 'conway.js'); + await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId }]); + + const { instance } = Array.from(watcher.watchers)[0]; + assert.strictEqual(instance.isReusingRecursiveWatcher, true); + assert.strictEqual(recursiveInstance.subscriptionsCount, 1); + + let changeFuture = awaitEvent(watcher, filePath, isMacintosh /* somehow fsevents seems to report still on the initial create from test setup */ ? FileChangeType.ADDED : FileChangeType.UPDATED, correlationId); + await Promises.writeFile(filePath, 'Hello World'); + await changeFuture; + + await recursiveWatcher.stop(); + recursiveWatcher.dispose(); + + await timeout(500); // give the watcher some time to restart + + changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, correlationId); + await Promises.writeFile(filePath, 'Hello World'); + await changeFuture; + + assert.strictEqual(instance.isReusingRecursiveWatcher, false); + } + + test('correlated watch requests support suspend/resume (file, does not exist in beginning, parcel watcher reused)', async function () { + const recursiveWatcher = createParcelWatcher(); + await recursiveWatcher.watch([{ path: testDir, excludes: [], recursive: true }]); + + await createWatcher(recursiveWatcher); + + const filePath = join(testDir, 'not-found-2.txt'); + + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 }; + await watcher.watch([request]); + await onDidWatchFail; + assert.strictEqual(watcher.isSuspended(request), true); + + const changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, 1); + await Promises.writeFile(filePath, 'Hello World'); + await changeFuture; + + assert.strictEqual(watcher.isSuspended(request), false); + }); + + test('event type filter (file watch)', async function () { + const filePath = join(testDir, 'lorem.txt'); + const request = { path: filePath, excludes: [], recursive: false, filter: FileChangeFilter.UPDATED | FileChangeFilter.DELETED, correlationId: 1 }; + await watcher.watch([request]); + + // Change file + let changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, 1); + await Promises.writeFile(filePath, 'Hello Change'); + await changeFuture; + + // Delete file + changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); + await fs.promises.unlink(filePath); + await changeFuture; + }); + + test('event type filter (folder watch)', async function () { + const request = { path: testDir, excludes: [], recursive: false, filter: FileChangeFilter.UPDATED | FileChangeFilter.DELETED, correlationId: 1 }; + await watcher.watch([request]); + + // Change file + const filePath = join(testDir, 'lorem.txt'); + let changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, 1); + await Promises.writeFile(filePath, 'Hello Change'); + await changeFuture; + + // Delete file + changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); + await fs.promises.unlink(filePath); + await changeFuture; + }); }); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 18a1a538433..4370d82bf90 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { realpathSync } from 'fs'; +import assert from 'assert'; +import { realpathSync, promises } from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; import { dirname, join } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { Promises, RimRafMode } from 'vs/base/node/pfs'; -import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { FileChangeFilter, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher'; import { IRecursiveWatchRequest } from 'vs/platform/files/common/watcher'; import { getDriveLetter } from 'vs/base/common/extpath'; @@ -21,46 +21,51 @@ import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +export class TestParcelWatcher extends ParcelWatcher { + + protected override readonly suspendedWatchRequestPollingInterval = 100; + + private readonly _onDidWatch = this._register(new Emitter()); + readonly onDidWatch = this._onDidWatch.event; + + readonly onWatchFail = this._onDidWatchFail.event; + + testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): string[] { + + // Work with strings as paths to simplify testing + const requests: IRecursiveWatchRequest[] = paths.map(path => { + return { path, excludes, recursive: true }; + }); + + return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); + } + + protected override getUpdateWatchersDelay(): number { + return 0; + } + + protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { + await super.doWatch(requests); + await this.whenReady(); + + this._onDidWatch.fire(); + } + + async whenReady(): Promise { + for (const watcher of this.watchers) { + await watcher.ready; + } + } +} // this suite has shown flaky runs in Azure pipelines where // tasks would just hang and timeout after a while (not in // mocha but generally). as such they will run only on demand // whenever we update the watcher library. -((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (parcel)', () => { - - class TestParcelWatcher extends ParcelWatcher { - - protected override readonly suspendedWatchRequestPollingInterval = 100; - - private readonly _onDidWatch = this._register(new Emitter()); - readonly onDidWatch = this._onDidWatch.event; - - readonly onWatchFail = this._onDidWatchFail.event; - - testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): string[] { - - // Work with strings as paths to simplify testing - const requests: IRecursiveWatchRequest[] = paths.map(path => { - return { path, excludes, recursive: true }; - }); - - return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); - } - - protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { - await super.doWatch(requests); - await this.whenReady(); - - this._onDidWatch.fire(); - } - - async whenReady(): Promise { - for (const watcher of this.watchers) { - await watcher.ready; - } - } - } +suite.skip('File Watcher (parcel)', () => { let testDir: string; let watcher: TestParcelWatcher; @@ -86,11 +91,11 @@ import { Emitter, Event } from 'vs/base/common/event'; watcher.onDidError(e => { if (loggingEnabled) { - console.log(`[recursive watcher test error] ${e}`); + console.log(`[recursive watcher test error] ${e.error}`); } }); - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher'); + testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath; const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath; @@ -98,7 +103,18 @@ import { Emitter, Event } from 'vs/base/common/event'; }); teardown(async () => { + const watchers = watcher.watchers.size; + let stoppedInstances = 0; + for (const instance of watcher.watchers) { + Event.once(instance.onDidStop)(() => { + if (instance.stopped) { + stoppedInstances++; + } + }); + } + await watcher.stop(); + assert.strictEqual(stoppedInstances, watchers, 'All watchers must be stopped before the test ends'); watcher.dispose(); // Possible that the file watcher is still holding @@ -170,37 +186,68 @@ import { Emitter, Event } from 'vs/base/common/event'; } test('basics', async function () { - await watcher.watch([{ path: testDir, excludes: [], recursive: true }]); + const request = { path: testDir, excludes: [], recursive: true }; + await watcher.watch([request]); + assert.strictEqual(watcher.watchers.size, watcher.watchers.size); + + const instance = Array.from(watcher.watchers)[0]; + assert.strictEqual(request, instance.request); + assert.strictEqual(instance.failed, false); + assert.strictEqual(instance.stopped, false); + + const disposables = new DisposableStore(); + + const subscriptions1 = new Map(); + const subscriptions2 = new Map(); // New file const newFilePath = join(testDir, 'deep', 'newFile.txt'); + disposables.add(instance.subscribe(newFilePath, change => subscriptions1.set(change.resource.fsPath, change.type))); + disposables.add(instance.subscribe(newFilePath, change => subscriptions2.set(change.resource.fsPath, change.type))); // can subscribe multiple times + assert.strictEqual(instance.include(newFilePath), true); + assert.strictEqual(instance.exclude(newFilePath), false); let changeFuture: Promise = awaitEvent(watcher, newFilePath, FileChangeType.ADDED); await Promises.writeFile(newFilePath, 'Hello World'); await changeFuture; + assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.ADDED); + assert.strictEqual(subscriptions2.get(newFilePath), FileChangeType.ADDED); // New folder const newFolderPath = join(testDir, 'deep', 'New Folder'); + disposables.add(instance.subscribe(newFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type))); + const disposable = instance.subscribe(newFolderPath, change => subscriptions2.set(change.resource.fsPath, change.type)); + disposable.dispose(); + assert.strictEqual(instance.include(newFolderPath), true); + assert.strictEqual(instance.exclude(newFolderPath), false); changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED); - await Promises.mkdir(newFolderPath); + await promises.mkdir(newFolderPath); await changeFuture; + assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.ADDED); + assert.strictEqual(subscriptions2.has(newFolderPath), false /* subscription was disposed before the event */); // Rename file let renamedFilePath = join(testDir, 'deep', 'renamedFile.txt'); + disposables.add(instance.subscribe(renamedFilePath, change => subscriptions1.set(change.resource.fsPath, change.type))); changeFuture = Promise.all([ awaitEvent(watcher, newFilePath, FileChangeType.DELETED), awaitEvent(watcher, renamedFilePath, FileChangeType.ADDED) ]); await Promises.rename(newFilePath, renamedFilePath); await changeFuture; + assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.DELETED); + assert.strictEqual(subscriptions1.get(renamedFilePath), FileChangeType.ADDED); // Rename folder let renamedFolderPath = join(testDir, 'deep', 'Renamed Folder'); + disposables.add(instance.subscribe(renamedFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type))); changeFuture = Promise.all([ awaitEvent(watcher, newFolderPath, FileChangeType.DELETED), awaitEvent(watcher, renamedFolderPath, FileChangeType.ADDED) ]); await Promises.rename(newFolderPath, renamedFolderPath); await changeFuture; + assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.DELETED); + assert.strictEqual(subscriptions1.get(renamedFolderPath), FileChangeType.ADDED); // Rename file (same name, different case) const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt'); @@ -243,7 +290,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // Copy file const copiedFilepath = join(testDir, 'deep', 'copiedFile.txt'); changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.ADDED); - await Promises.copyFile(movedFilepath, copiedFilepath); + await promises.copyFile(movedFilepath, copiedFilepath); await changeFuture; // Copy folder @@ -265,28 +312,34 @@ import { Emitter, Event } from 'vs/base/common/event'; // Read file does not emit event changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-read-file'); - await Promises.readFile(anotherNewFilePath); + await promises.readFile(anotherNewFilePath); await Promise.race([timeout(100), changeFuture]); // Stat file does not emit event changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-stat'); - await Promises.stat(anotherNewFilePath); + await promises.stat(anotherNewFilePath); await Promise.race([timeout(100), changeFuture]); // Stat folder does not emit event changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.UPDATED, 'unexpected-event-from-stat'); - await Promises.stat(copiedFolderpath); + await promises.stat(copiedFolderpath); await Promise.race([timeout(100), changeFuture]); // Delete file changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED); - await Promises.unlink(copiedFilepath); + disposables.add(instance.subscribe(copiedFilepath, change => subscriptions1.set(change.resource.fsPath, change.type))); + await promises.unlink(copiedFilepath); await changeFuture; + assert.strictEqual(subscriptions1.get(copiedFilepath), FileChangeType.DELETED); // Delete folder changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED); - await Promises.rmdir(copiedFolderpath); + disposables.add(instance.subscribe(copiedFolderpath, change => subscriptions1.set(change.resource.fsPath, change.type))); + await promises.rmdir(copiedFolderpath); await changeFuture; + assert.strictEqual(subscriptions1.get(copiedFolderpath), FileChangeType.DELETED); + + disposables.dispose(); }); (isMacintosh /* this test seems not possible with fsevents backend */ ? test.skip : test)('basics (atomic writes)', async function () { @@ -295,7 +348,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // Delete + Recreate file const newFilePath = join(testDir, 'deep', 'conway.js'); const changeFuture = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED); - await Promises.unlink(newFilePath); + await promises.unlink(newFilePath); Promises.writeFile(newFilePath, 'Hello Atomic World'); await changeFuture; }); @@ -320,13 +373,13 @@ import { Emitter, Event } from 'vs/base/common/event'; // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, correlationId, expectedCount); - await Promises.unlink(filePath); + await promises.unlink(filePath); await changeFuture; } test('multiple events', async function () { await watcher.watch([{ path: testDir, excludes: [], recursive: true }]); - await Promises.mkdir(join(testDir, 'deep-multiple')); + await promises.mkdir(join(testDir, 'deep-multiple')); // multiple add @@ -396,12 +449,12 @@ import { Emitter, Event } from 'vs/base/common/event'; const deleteFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.DELETED); await Promise.all([ - await Promises.unlink(newFilePath1), - await Promises.unlink(newFilePath2), - await Promises.unlink(newFilePath3), - await Promises.unlink(newFilePath4), - await Promises.unlink(newFilePath5), - await Promises.unlink(newFilePath6) + await promises.unlink(newFilePath1), + await promises.unlink(newFilePath2), + await promises.unlink(newFilePath3), + await promises.unlink(newFilePath4), + await promises.unlink(newFilePath5), + await promises.unlink(newFilePath6) ]); await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3, deleteFuture4, deleteFuture5, deleteFuture6]); @@ -510,7 +563,7 @@ import { Emitter, Event } from 'vs/base/common/event'; (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); - await Promises.symlink(linkTarget, link); + await promises.symlink(linkTarget, link); await watcher.watch([{ path: link, excludes: [], recursive: true }]); @@ -520,7 +573,7 @@ import { Emitter, Event } from 'vs/base/common/event'; (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (via extra watch)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); - await Promises.symlink(linkTarget, link); + await promises.symlink(linkTarget, link); await watcher.watch([{ path: testDir, excludes: [], recursive: true }, { path: link, excludes: [], recursive: true }]); @@ -564,7 +617,7 @@ import { Emitter, Event } from 'vs/base/common/event'; // Restore watched path await timeout(1500); // node.js watcher used for monitoring folder restore is async - await Promises.mkdir(watchedPath); + await promises.mkdir(watchedPath); await timeout(1500); // restart is delayed await watcher.whenReady(); @@ -676,26 +729,59 @@ import { Emitter, Event } from 'vs/base/common/event'; await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + let failed = false; + const instance = Array.from(watcher.watchers)[0]; + assert.strictEqual(instance.include(folderPath), true); + instance.onDidFail(() => failed = true); + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1); Promises.rm(folderPath, RimRafMode.UNLINK); await onDidWatchFail; await changeFuture; + assert.strictEqual(failed, true); + assert.strictEqual(instance.failed, true); }); - test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async () => { + test('correlated watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => { + await testCorrelatedWatchFolderDoesNotExist(false); + }); + + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { + await testCorrelatedWatchFolderDoesNotExist(true); + }); + + async function testCorrelatedWatchFolderDoesNotExist(reuseExistingWatcher: boolean) { let onDidWatchFail = Event.toPromise(watcher.onWatchFail); const folderPath = join(testDir, 'not-found'); - await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + + const requests: IRecursiveWatchRequest[] = []; + if (reuseExistingWatcher) { + requests.push({ path: testDir, excludes: [], recursive: true }); + await watcher.watch(requests); + } + + const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true, correlationId: 1 }; + requests.push(request); + + await watcher.watch(requests); await onDidWatchFail; + if (reuseExistingWatcher) { + assert.strictEqual(watcher.isSuspended(request), true); + } else { + assert.strictEqual(watcher.isSuspended(request), 'polling'); + } + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); let onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await promises.mkdir(folderPath); await changeFuture; await onDidWatch; + assert.strictEqual(watcher.isSuspended(request), false); + const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath, 1); @@ -705,16 +791,30 @@ import { Emitter, Event } from 'vs/base/common/event'; changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await promises.mkdir(folderPath); await changeFuture; await onDidWatch; await basicCrudTest(filePath, 1); + } + + test('correlated watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => { + await testCorrelatedWatchFolderExists(false); }); - test('correlated watch requests support suspend/resume (folder, exist in beginning)', async () => { + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { + await testCorrelatedWatchFolderExists(true); + }); + + async function testCorrelatedWatchFolderExists(reuseExistingWatcher: boolean) { const folderPath = join(testDir, 'deep'); - await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]); + + const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]; + if (reuseExistingWatcher) { + requests.push({ path: testDir, excludes: [], recursive: true }); + } + + await watcher.watch(requests); const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath, 1); @@ -725,10 +825,46 @@ import { Emitter, Event } from 'vs/base/common/event'; const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); const onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await promises.mkdir(folderPath); await changeFuture; await onDidWatch; await basicCrudTest(filePath, 1); + } + + test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () { + const folderPath1 = join(testDir, 'deep', 'not-existing1'); + const folderPath2 = join(testDir, 'deep', 'not-existing2'); + const folderPath3 = join(testDir, 'not-existing3'); + + const requests: IRecursiveWatchRequest[] = [ + { path: folderPath1, excludes: [], recursive: true, correlationId: 1 }, + { path: folderPath2, excludes: [], recursive: true, correlationId: 2 }, + { path: folderPath3, excludes: [], recursive: true, correlationId: 3 }, + { path: join(testDir, 'deep'), excludes: [], recursive: true } + ]; + + await watcher.watch(requests); + + assert.strictEqual(watcher.isSuspended(requests[0]), true); + assert.strictEqual(watcher.isSuspended(requests[1]), true); + assert.strictEqual(watcher.isSuspended(requests[2]), 'polling'); + assert.strictEqual(watcher.isSuspended(requests[3]), false); + }); + + test('event type filter', async function () { + const request = { path: testDir, excludes: [], recursive: true, filter: FileChangeFilter.ADDED | FileChangeFilter.DELETED, correlationId: 1 }; + await watcher.watch([request]); + + // Change file + const filePath = join(testDir, 'lorem-newfile.txt'); + let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, undefined, 1); + await Promises.writeFile(filePath, 'Hello Change'); + await changeFuture; + + // Delete file + changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, 1); + await promises.unlink(filePath); + await changeFuture; }); }); diff --git a/src/vs/platform/hover/browser/hover.ts b/src/vs/platform/hover/browser/hover.ts index 9213e1ea02f..51ead712fcb 100644 --- a/src/vs/platform/hover/browser/hover.ts +++ b/src/vs/platform/hover/browser/hover.ts @@ -4,234 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { addStandardDisposableListener } from 'vs/base/browser/dom'; +import { addStandardDisposableListener, isHTMLElement } from 'vs/base/browser/dom'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import type { IHoverDelegate2, IHoverOptions, IHoverWidget } from 'vs/base/browser/ui/hover/hover'; export const IHoverService = createDecorator('hoverService'); -/** - * Enables the convenient display of rich markdown-based hovers in the workbench. - */ -export interface IHoverService { +export interface IHoverService extends IHoverDelegate2 { readonly _serviceBrand: undefined; - - /** - * Shows a hover, provided a hover with the same options object is not already visible. - * @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; - - /** - * Hides the hover if it was visible. This call will be ignored if the the hover is currently - * "locked" via the alt/option key. - */ - hideHover(): void; - - /** - * This should only be used until we have the ability to show multiple context views - * simultaneously. #188822 - */ - showAndFocusLastHover(): void; -} - -export interface IHoverOptions { - /** - * The content to display in the primary section of the hover. The type of text determines the - * default `hideOnHover` behavior. - */ - content: IMarkdownString | string | HTMLElement; - - /** - * The target for the hover. This determines the position of the hover and it will only be - * hidden when the mouse leaves both the hover and the target. A HTMLElement can be used for - * simple cases and a IHoverTarget for more complex cases where multiple elements and/or a - * dispose method is required. - */ - target: IHoverTarget | HTMLElement; - - /* - * The container to pass to {@link IContextViewProvider.showContextView} which renders the hover - * in. This is particularly useful for more natural tab focusing behavior, where the hover is - * created as the next tab index after the element being hovered and/or to workaround the - * element's container hiding on `focusout`. - */ - container?: HTMLElement; - - /** - * An ID to associate with the hover to be used as an equality check. Normally when calling - * {@link IHoverService.showHover} the options object itself is used to determine if the hover - * is the same one that is already showing, when this is set, the ID will be used instead. - */ - id?: number | string; - - /** - * A set of actions for the hover's "status bar". - */ - actions?: IHoverAction[]; - - /** - * An optional array of classes to add to the hover element. - */ - additionalClasses?: string[]; - - /** - * An optional link handler for markdown links, if this is not provided the IOpenerService will - * be used to open the links using its default options. - */ - linkHandler?(url: string): void; - - /** - * Whether to trap focus in the following ways: - * - When the hover closes, focus goes to the element that had focus before the hover opened - * - If there are elements in the hover to focus, focus stays inside of the hover when tabbing - * Note that this is overridden to true when in screen reader optimized mode. - */ - trapFocus?: boolean; - - /** - * Options that defines where the hover is positioned. - */ - position?: IHoverPositionOptions; - - /** - * Options that defines how long the hover is shown and when it hides. - */ - persistence?: IHoverPersistenceOptions; - - /** - * Options that define how the hover looks. - */ - appearance?: IHoverAppearanceOptions; -} - -export interface IHoverPositionOptions { - /** - * Position of the hover. The default is to show above the target. This option will be ignored - * if there is not enough room to layout the hover in the specified position, unless the - * forcePosition option is set. - */ - hoverPosition?: HoverPosition; - - /** - * Force the hover position, reducing the size of the hover instead of adjusting the hover - * position. - */ - forcePosition?: boolean; -} - -export interface IHoverPersistenceOptions { - /** - * Whether to hide the hover when the mouse leaves the `target` and enters the actual hover. - * This is false by default when text is an `IMarkdownString` and true when `text` is a - * `string`. Note that this will be ignored if any `actions` are provided as hovering is - * required to make them accessible. - * - * In general hiding on hover is desired for: - * - Regular text where selection is not important - * - Markdown that contains no links where selection is not important - */ - hideOnHover?: boolean; - - /** - * Whether to hide the hover when a key is pressed. - */ - hideOnKeyDown?: boolean; - - /** - * Whether to make the hover sticky, this means it will not be hidden when the mouse leaves the - * hover. - */ - sticky?: boolean; -} - -export interface IHoverAppearanceOptions { - /** - * Whether to show the hover pointer, a little arrow that connects the target and the hover. - */ - showPointer?: boolean; - - /** - * Whether to show a compact hover, reducing the font size and padding of the hover. - */ - compact?: boolean; - - /** - * When {@link hideOnHover} is explicitly true or undefined and its auto value is detected to - * hide, show a hint at the bottom of the hover explaining how to mouse over the widget. This - * should be used in the cases where despite the hover having no interactive content, it's - * likely the user may want to interact with it somehow. - */ - showHoverHint?: boolean; - - /** - * Whether to skip the fade in animation, this should be used when hovering from one hover to - * another in the same group so it looks like the hover is moving from one element to the other. - */ - skipFadeInAnimation?: boolean; -} - -export interface IHoverAction { - /** - * The label to use in the hover's status bar. - */ - label: string; - - /** - * The command ID of the action, this is used to resolve the keybinding to display after the - * action label. - */ - commandId: string; - - /** - * An optional class of an icon that will be displayed before the label. - */ - iconClass?: string; - - /** - * The callback to run the action. - * @param target The action element that was activated. - */ - run(target: HTMLElement): void; -} - -/** - * A target for a hover. - */ -export interface IHoverTarget extends IDisposable { - /** - * A set of target elements used to position the hover. If multiple elements are used the hover - * will try to not overlap any target element. An example use case for this is show a hover for - * wrapped text. - */ - readonly targetElements: readonly HTMLElement[]; - - /** - * An optional absolute x coordinate to position the hover with, for example to position the - * hover using `MouseEvent.pageX`. - */ - x?: number; - - /** - * An optional absolute y coordinate to position the hover with, for example to position the - * hover using `MouseEvent.pageY`. - */ - y?: number; } export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate { @@ -247,7 +30,7 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate return this._delay; } - private hoverDisposables = this._register(new DisposableStore()); + private readonly hoverDisposables = this._register(new DisposableStore()); constructor( public readonly placement: 'mouse' | 'element', @@ -271,7 +54,7 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate // close hover on escape this.hoverDisposables.clear(); - const targets = options.target instanceof HTMLElement ? [options.target] : options.target.targetElements; + const targets = isHTMLElement(options.target) ? [options.target] : options.target.targetElements; for (const target of targets) { this.hoverDisposables.add(addStandardDisposableListener(target, 'keydown', (e) => { if (e.equals(KeyCode.Escape)) { @@ -280,7 +63,7 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate })); } - const id = options.content instanceof HTMLElement ? undefined : options.content.toString(); + const id = isHTMLElement(options.content) ? undefined : options.content.toString(); return this.hoverService.showHover({ ...options, diff --git a/src/vs/platform/hover/test/browser/nullHoverService.ts b/src/vs/platform/hover/test/browser/nullHoverService.ts new file mode 100644 index 00000000000..44c73f8a0a6 --- /dev/null +++ b/src/vs/platform/hover/test/browser/nullHoverService.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 { Disposable } from 'vs/base/common/lifecycle'; +import type { IHoverService } from 'vs/platform/hover/browser/hover'; + +export const NullHoverService: IHoverService = { + _serviceBrand: undefined, + hideHover: () => undefined, + showHover: () => undefined, + setupManagedHover: () => Disposable.None as any, + showAndFocusLastHover: () => undefined, + showManagedHover: () => undefined +}; diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index f6113a5ecd1..c1ce6565271 100644 --- a/src/vs/platform/instantiation/common/instantiation.ts +++ b/src/vs/platform/instantiation/common/instantiation.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore } from 'vs/base/common/lifecycle'; import * as descriptors from './descriptors'; import { ServiceCollection } from './serviceCollection'; @@ -61,8 +62,21 @@ export interface IInstantiationService { /** * Creates a child of this service which inherits all current services * and adds/overwrites the given services. + * + * NOTE that the returned child is `disposable` and should be disposed when not used + * anymore. This will also dispose all the services that this service has created. */ - createChild(services: ServiceCollection): IInstantiationService; + createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService; + + /** + * Disposes this instantiation service. + * + * - Will dispose all services that this instantiation service has created. + * - Will dispose all its children but not its parent. + * - Will NOT dispose services-instances that this service has been created with + * - Will NOT dispose consumer-instances this service has created + */ + dispose(): void; } diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index ef9ae6601ce..a61ef74333d 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -6,7 +6,7 @@ import { GlobalIdleValue } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { illegalState } from 'vs/base/common/errors'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { SyncDescriptor, SyncDescriptor0 } from 'vs/platform/instantiation/common/descriptors'; import { Graph } from 'vs/platform/instantiation/common/graph'; import { GetLeadingNonServiceArgs, IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from 'vs/platform/instantiation/common/instantiation'; @@ -32,6 +32,10 @@ export class InstantiationService implements IInstantiationService { readonly _globalGraph?: Graph; private _globalGraphImplicitDependency?: string; + private _isDisposed = false; + private readonly _servicesToMaybeDispose = new Set(); + private readonly _children = new Set(); + constructor( private readonly _services: ServiceCollection = new ServiceCollection(), private readonly _strict: boolean = false, @@ -43,11 +47,48 @@ export class InstantiationService implements IInstantiationService { this._globalGraph = _enableTracing ? _parent?._globalGraph ?? new Graph(e => e) : undefined; } - createChild(services: ServiceCollection): IInstantiationService { - return new InstantiationService(services, this._strict, this, this._enableTracing); + dispose(): void { + if (!this._isDisposed) { + this._isDisposed = true; + // dispose all child services + dispose(this._children); + this._children.clear(); + + // dispose all services created by this service + for (const candidate of this._servicesToMaybeDispose) { + if (isDisposable(candidate)) { + candidate.dispose(); + } + } + this._servicesToMaybeDispose.clear(); + } + } + + private _throwIfDisposed(): void { + if (this._isDisposed) { + throw new Error('InstantiationService has been disposed'); + } + } + + createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService { + this._throwIfDisposed(); + + const that = this; + const result = new class extends InstantiationService { + override dispose(): void { + that._children.delete(result); + super.dispose(); + } + }(services, this._strict, this, this._enableTracing); + this._children.add(result); + + store?.add(result); + return result; } invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R { + this._throwIfDisposed(); + const _trace = Trace.traceInvocation(this._enableTracing, fn); let _done = false; try { @@ -75,6 +116,8 @@ export class InstantiationService implements IInstantiationService { createInstance(descriptor: SyncDescriptor0): T; createInstance any, R extends InstanceType>(ctor: Ctor, ...args: GetLeadingNonServiceArgs>): R; createInstance(ctorOrDescriptor: any | SyncDescriptor, ...rest: any[]): any { + this._throwIfDisposed(); + let _trace: Trace; let result: any; if (ctorOrDescriptor instanceof SyncDescriptor) { @@ -119,11 +162,11 @@ export class InstantiationService implements IInstantiationService { return Reflect.construct(ctor, args.concat(serviceArgs)); } - private _setServiceInstance(id: ServiceIdentifier, instance: T): void { + private _setCreatedServiceInstance(id: ServiceIdentifier, instance: T): void { if (this._services.get(id) instanceof SyncDescriptor) { this._services.set(id, instance); } else if (this._parent) { - this._parent._setServiceInstance(id, instance); + this._parent._setCreatedServiceInstance(id, instance); } else { throw new Error('illegalState - setting UNKNOWN service instance'); } @@ -173,8 +216,15 @@ export class InstantiationService implements IInstantiationService { let cycleCount = 0; const stack = [{ id, desc, _trace }]; + const seen = new Set(); while (stack.length) { const item = stack.pop()!; + + if (seen.has(String(item.id))) { + continue; + } + seen.add(String(item.id)); + graph.lookupOrInsertNode(item); // a weak but working heuristic for cycle checks @@ -221,7 +271,7 @@ export class InstantiationService implements IInstantiationService { if (instanceOrDesc instanceof SyncDescriptor) { // create instance and overwrite the service collections const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace); - this._setServiceInstance(data.id, instance); + this._setCreatedServiceInstance(data.id, instance); } graph.removeNode(data); } @@ -231,7 +281,7 @@ export class InstantiationService implements IInstantiationService { private _createServiceInstanceWithOwner(id: ServiceIdentifier, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T { if (this._services.get(id) instanceof SyncDescriptor) { - return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace); + return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace, this._servicesToMaybeDispose); } else if (this._parent) { return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation, _trace); } else { @@ -239,10 +289,12 @@ export class InstantiationService implements IInstantiationService { } } - private _createServiceInstance(id: ServiceIdentifier, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T { + private _createServiceInstance(id: ServiceIdentifier, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace, disposeBucket: Set): T { if (!supportsDelayedInstantiation) { // eager instantiation - return this._createInstance(ctor, args, _trace); + const result = this._createInstance(ctor, args, _trace); + disposeBucket.add(result); + return result; } else { const child = new InstantiationService(undefined, this._strict, this, this._enableTracing); @@ -274,7 +326,7 @@ export class InstantiationService implements IInstantiationService { } } earlyListeners.clear(); - + disposeBucket.add(result); return result; }); return new Proxy(Object.create(null), { diff --git a/src/vs/platform/instantiation/test/common/graph.test.ts b/src/vs/platform/instantiation/test/common/graph.test.ts index c5abc22ccff..99e8e0e75a0 100644 --- a/src/vs/platform/instantiation/test/common/graph.test.ts +++ b/src/vs/platform/instantiation/test/common/graph.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Graph } from 'vs/platform/instantiation/common/graph'; diff --git a/src/vs/platform/instantiation/test/common/instantiationService.test.ts b/src/vs/platform/instantiation/test/common/instantiationService.test.ts index d3fa8924baa..70718ba712d 100644 --- a/src/vs/platform/instantiation/test/common/instantiationService.test.ts +++ b/src/vs/platform/instantiation/test/common/instantiationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -655,5 +655,163 @@ suite('Instantiation Service', () => { assert.strictEqual(eventCount, 1); }); + + test('Dispose services it created', function () { + let disposedA = false; + let disposedB = false; + + const A = createDecorator
('A'); + interface A { + _serviceBrand: undefined; + value: 1; + } + class AImpl implements A { + _serviceBrand: undefined; + value: 1 = 1; + dispose() { + disposedA = true; + } + } + + const B = createDecorator('B'); + interface B { + _serviceBrand: undefined; + value: 1; + } + class BImpl implements B { + _serviceBrand: undefined; + value: 1 = 1; + dispose() { + disposedB = true; + } + } + + const insta = new InstantiationService(new ServiceCollection( + [A, new SyncDescriptor(AImpl, undefined, true)], + [B, new BImpl()], + ), true, undefined, true); + + class Consumer { + constructor( + @A public readonly a: A, + @B public readonly b: B + ) { + assert.strictEqual(a.value, b.value); + } + } + + const c: Consumer = insta.createInstance(Consumer); + + insta.dispose(); + assert.ok(c); + assert.strictEqual(disposedA, true); + assert.strictEqual(disposedB, false); + }); + + test('Disposed service cannot be used anymore', function () { + + + const B = createDecorator('B'); + interface B { + _serviceBrand: undefined; + value: 1; + } + class BImpl implements B { + _serviceBrand: undefined; + value: 1 = 1; + } + + const insta = new InstantiationService(new ServiceCollection( + [B, new BImpl()], + ), true, undefined, true); + + class Consumer { + constructor( + @B public readonly b: B + ) { + assert.strictEqual(b.value, 1); + } + } + + const c: Consumer = insta.createInstance(Consumer); + assert.ok(c); + + insta.dispose(); + + assert.throws(() => insta.createInstance(Consumer)); + assert.throws(() => insta.invokeFunction(accessor => { })); + assert.throws(() => insta.createChild(new ServiceCollection())); + }); + + test('Child does not dispose parent', function () { + + const B = createDecorator('B'); + interface B { + _serviceBrand: undefined; + value: 1; + } + class BImpl implements B { + _serviceBrand: undefined; + value: 1 = 1; + } + + const insta1 = new InstantiationService(new ServiceCollection( + [B, new BImpl()], + ), true, undefined, true); + + const insta2 = insta1.createChild(new ServiceCollection()); + + class Consumer { + constructor( + @B public readonly b: B + ) { + assert.strictEqual(b.value, 1); + } + } + + assert.ok(insta1.createInstance(Consumer)); + assert.ok(insta2.createInstance(Consumer)); + + insta2.dispose(); + + assert.ok(insta1.createInstance(Consumer)); // parent NOT disposed by child + assert.throws(() => insta2.createInstance(Consumer)); + }); + + test('Parent does dispose children', function () { + + const B = createDecorator('B'); + interface B { + _serviceBrand: undefined; + value: 1; + } + class BImpl implements B { + _serviceBrand: undefined; + value: 1 = 1; + } + + const insta1 = new InstantiationService(new ServiceCollection( + [B, new BImpl()], + ), true, undefined, true); + + const insta2 = insta1.createChild(new ServiceCollection()); + + class Consumer { + constructor( + @B public readonly b: B + ) { + assert.strictEqual(b.value, 1); + } + } + + assert.ok(insta1.createInstance(Consumer)); + assert.ok(insta2.createInstance(Consumer)); + + insta1.dispose(); + + assert.throws(() => insta2.createInstance(Consumer)); // child is disposed by parent + assert.throws(() => insta1.createInstance(Consumer)); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts b/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts index a639165802d..a7c52175e9d 100644 --- a/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts +++ b/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts @@ -21,7 +21,7 @@ export class TestInstantiationService extends InstantiationService implements ID private _servciesMap: Map, any>; - constructor(private _serviceCollection: ServiceCollection = new ServiceCollection(), strict: boolean = false, parent?: TestInstantiationService) { + constructor(private _serviceCollection: ServiceCollection = new ServiceCollection(), strict: boolean = false, parent?: TestInstantiationService, private _properDispose?: boolean) { super(_serviceCollection, strict, parent); this._servciesMap = new Map, any>(); @@ -130,8 +130,11 @@ export class TestInstantiationService extends InstantiationService implements ID return new TestInstantiationService(services, false, this); } - dispose() { + override dispose() { sinon.restore(); + if (this._properDispose) { + super.dispose(); + } } } diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index df2a1e27599..26f8e8004ee 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; +import { UriComponents } from 'vs/base/common/uri'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; import { PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -19,7 +19,7 @@ export interface WindowData { zoomLevel: number; } -export const enum IssueType { +export const enum OldIssueType { Bug, PerformanceIssue, FeatureRequest @@ -31,7 +31,7 @@ export enum IssueSource { Marketplace = 'marketplace' } -export interface IssueReporterStyles extends WindowStyles { +export interface OldIssueReporterStyles extends WindowStyles { textLinkColor?: string; textLinkActiveForeground?: string; inputBackground?: string; @@ -49,7 +49,7 @@ export interface IssueReporterStyles extends WindowStyles { sliderActiveColor?: string; } -export interface IssueReporterExtensionData { +export interface OldIssueReporterExtensionData { name: string; publisher: string | undefined; version: string; @@ -61,16 +61,14 @@ export interface IssueReporterExtensionData { bugsUrl: string | undefined; extensionData?: string; extensionTemplate?: string; - hasIssueUriRequestHandler?: boolean; - hasIssueDataProviders?: boolean; data?: string; uri?: UriComponents; } -export interface IssueReporterData extends WindowData { - styles: IssueReporterStyles; - enabledExtensions: IssueReporterExtensionData[]; - issueType?: IssueType; +export interface OldIssueReporterData extends WindowData { + styles: OldIssueReporterStyles; + enabledExtensions: OldIssueReporterExtensionData[]; + issueType?: OldIssueType; issueSource?: IssueSource; extensionId?: string; experiments?: string; @@ -111,9 +109,9 @@ export interface ProcessExplorerData extends WindowData { applicationName: string; } -export interface IssueReporterWindowConfiguration extends ISandboxConfiguration { +export interface OldIssueReporterWindowConfiguration extends ISandboxConfiguration { disableExtensions: boolean; - data: IssueReporterData; + data: OldIssueReporterData; os: { type: string; arch: string; @@ -129,22 +127,24 @@ export const IIssueMainService = createDecorator('issueServic export interface IIssueMainService { readonly _serviceBrand: undefined; - stopTracing(): Promise; - openReporter(data: IssueReporterData): Promise; - openProcessExplorer(data: ProcessExplorerData): Promise; - getSystemStatus(): Promise; - // Used by the issue reporter - - $getSystemInfo(): Promise; - $getPerformanceInfo(): Promise; + openReporter(data: OldIssueReporterData): Promise; $reloadWithExtensionsDisabled(): Promise; $showConfirmCloseDialog(): Promise; $showClipboardDialog(): Promise; - $getIssueReporterUri(extensionId: string): Promise; - $getIssueReporterData(extensionId: string): Promise; - $getIssueReporterTemplate(extensionId: string): Promise; - $getReporterStatus(extensionId: string, extensionName: string): Promise; - $sendReporterMenu(extensionId: string, extensionName: string): Promise; + $sendReporterMenu(extensionId: string, extensionName: string): Promise; $closeReporter(): Promise; } + +export const IProcessMainService = createDecorator('processService'); + +export interface IProcessMainService { + readonly _serviceBrand: undefined; + getSystemStatus(): Promise; + stopTracing(): Promise; + openProcessExplorer(data: ProcessExplorerData): Promise; + + // Used by the process explorer + $getSystemInfo(): Promise; + $getPerformanceInfo(): Promise; +} diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 0bcf0f31928..1aea4ead9b9 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -3,36 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, BrowserWindowConstructorOptions, contentTracing, Display, IpcMainEvent, screen } from 'electron'; +import { BrowserWindow, BrowserWindowConstructorOptions, Display, screen } from 'electron'; import { arch, release, type } from 'os'; -import { Promises, raceTimeout, timeout } from 'vs/base/common/async'; +import { raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { randomPath } from 'vs/base/common/extpath'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; -import { URI } from 'vs/base/common/uri'; -import { listProcesses } from 'vs/base/node/ps'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { localize } from 'vs/nls'; -import { IDiagnosticsService, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { IIssueMainService, IssueReporterData, IssueReporterWindowConfiguration, ProcessExplorerData, ProcessExplorerWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, OldIssueReporterData, OldIssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import product from 'vs/platform/product/common/product'; -import { IProductService } from 'vs/platform/product/common/productService'; import { IIPCObjectUrl, IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; -import { IStateService } from 'vs/platform/state/node/state'; -import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; import { ICodeWindow, IWindowState } from 'vs/platform/window/electron-main/window'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -const processExplorerWindowState = 'issue.processExplorerWindowState'; - interface IBrowserWindowOptions { backgroundColor: string | undefined; title: string; @@ -51,104 +41,25 @@ export class IssueMainService implements IIssueMainService { private issueReporterWindow: BrowserWindow | null = null; private issueReporterParentWindow: BrowserWindow | null = null; - private processExplorerWindow: BrowserWindow | null = null; - private processExplorerParentWindow: BrowserWindow | null = null; - constructor( private userEnv: IProcessEnvironment, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILogService private readonly logService: ILogService, - @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, - @IDiagnosticsMainService private readonly diagnosticsMainService: IDiagnosticsMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @IProtocolMainService private readonly protocolMainService: IProtocolMainService, - @IProductService private readonly productService: IProductService, - @IStateService private readonly stateService: IStateService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - ) { - this.registerListeners(); - } - - //#region Register Listeners - - private registerListeners(): void { - validatedIpcMain.on('vscode:listProcesses', async event => { - const processes = []; - - try { - processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(process.pid) }); - - const remoteDiagnostics = await this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true }); - remoteDiagnostics.forEach(data => { - if (isRemoteDiagnosticError(data)) { - processes.push({ - name: data.hostName, - rootProcess: data - }); - } else { - if (data.processes) { - processes.push({ - name: data.hostName, - rootProcess: data.processes - }); - } - } - }); - } catch (e) { - this.logService.error(`Listing processes failed: ${e}`); - } - - this.safeSend(event, 'vscode:listProcessesResponse', processes); - }); - - validatedIpcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { - const { id, from, args } = commandInfo; - - let parentWindow: BrowserWindow | null; - switch (from) { - case 'processExplorer': - parentWindow = this.processExplorerParentWindow; - break; - default: - // The issue reporter does not use this anymore. - throw new Error(`Unexpected command source: ${from}`); - } - - parentWindow?.webContents.send('vscode:runAction', { id, from, args }); - }); - - validatedIpcMain.on('vscode:closeProcessExplorer', event => { - this.processExplorerWindow?.close(); - }); - - validatedIpcMain.on('vscode:pidToNameRequest', async event => { - const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics(); - - const pidToNames: [number, string][] = []; - for (const window of mainProcessInfo.windows) { - pidToNames.push([window.pid, `window [${window.id}] (${window.title})`]); - } - - for (const { pid, name } of UtilityProcess.getAll()) { - pidToNames.push([pid, name]); - } - - this.safeSend(event, 'vscode:pidToNameResponse', pidToNames); - }); - } - - //#endregion + ) { } //#region Used by renderer - async openReporter(data: IssueReporterData): Promise { + async openReporter(data: OldIssueReporterData): Promise { if (!this.issueReporterWindow) { this.issueReporterParentWindow = BrowserWindow.getFocusedWindow(); if (this.issueReporterParentWindow) { const issueReporterDisposables = new DisposableStore(); - const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl()); + const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl()); const position = this.getWindowPosition(this.issueReporterParentWindow, 700, 800); this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, { @@ -170,11 +81,15 @@ export class IssueMainService implements IIssueMainService { arch: arch(), release: release(), }, - product + product, + nls: { + messages: globalThis._VSCODE_NLS_MESSAGES, + language: globalThis._VSCODE_NLS_LANGUAGE + } }); this.issueReporterWindow.loadURL( - FileAccess.asBrowserUri(`vs/code/electron-sandbox/issue/issueReporter${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) + FileAccess.asBrowserUri(`vs/workbench/contrib/issue/electron-sandbox/issueReporter${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) ); this.issueReporterWindow.on('close', () => { @@ -197,125 +112,9 @@ export class IssueMainService implements IIssueMainService { } } - async openProcessExplorer(data: ProcessExplorerData): Promise { - if (!this.processExplorerWindow) { - this.processExplorerParentWindow = BrowserWindow.getFocusedWindow(); - if (this.processExplorerParentWindow) { - const processExplorerDisposables = new DisposableStore(); - - const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); - - const savedPosition = this.stateService.getItem(processExplorerWindowState, undefined); - const position = isStrictWindowState(savedPosition) ? savedPosition : this.getWindowPosition(this.processExplorerParentWindow, 800, 500); - - this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { - backgroundColor: data.styles.backgroundColor, - title: localize('processExplorer', "Process Explorer"), - zoomLevel: data.zoomLevel, - alwaysOnTop: true - }, 'process-explorer'); - - // Store into config object URL - processExplorerWindowConfigUrl.update({ - appRoot: this.environmentMainService.appRoot, - windowId: this.processExplorerWindow.id, - userEnv: this.userEnv, - data, - product - }); - - this.processExplorerWindow.loadURL( - FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) - ); - - this.processExplorerWindow.on('close', () => { - this.processExplorerWindow = null; - processExplorerDisposables.dispose(); - }); - - this.processExplorerParentWindow.on('close', () => { - if (this.processExplorerWindow) { - this.processExplorerWindow.close(); - this.processExplorerWindow = null; - - processExplorerDisposables.dispose(); - } - }); - - const storeState = () => { - if (!this.processExplorerWindow) { - return; - } - const size = this.processExplorerWindow.getSize(); - const position = this.processExplorerWindow.getPosition(); - if (!size || !position) { - return; - } - const state: IWindowState = { - width: size[0], - height: size[1], - x: position[0], - y: position[1] - }; - this.stateService.setItem(processExplorerWindowState, state); - }; - - this.processExplorerWindow.on('moved', storeState); - this.processExplorerWindow.on('resized', storeState); - } - } - - if (this.processExplorerWindow) { - this.focusWindow(this.processExplorerWindow); - } - } - - async stopTracing(): Promise { - if (!this.environmentMainService.args.trace) { - return; // requires tracing to be on - } - - const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); - - // Inform user to report an issue - await this.dialogMainService.showMessageBox({ - type: 'info', - message: localize('trace.message', "Successfully created the trace file"), - detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], - }, BrowserWindow.getFocusedWindow() ?? undefined); - - // Show item in explorer - this.nativeHostMainService.showItemInFolder(undefined, path); - } - - async getSystemStatus(): Promise { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); - - return this.diagnosticsService.getDiagnostics(info, remoteData); - } - //#endregion //#region used by issue reporter window - - async $getSystemInfo(): Promise { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); - const msg = await this.diagnosticsService.getSystemInfo(info, remoteData); - return msg; - } - - async $getPerformanceInfo(): Promise { - try { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]); - return await this.diagnosticsService.getPerformanceInfo(info, remoteData); - } catch (error) { - this.logService.warn('issueService#getPerformanceInfo ', error.message); - - throw error; - } - } - async $reloadWithExtensionsDisabled(): Promise { if (this.issueReporterParentWindow) { try { @@ -374,106 +173,22 @@ export class IssueMainService implements IIssueMainService { return window; } - async $getIssueReporterUri(extensionId: string): Promise { - const window = this.issueReporterWindowCheck(); - const replyChannel = `vscode:triggerIssueUriRequestHandlerResponse${window.id}`; - return Promises.withAsyncBody(async (resolve, reject) => { - - const cts = new CancellationTokenSource(); - window.sendWhenReady('vscode:triggerIssueUriRequestHandler', cts.token, { replyChannel, extensionId }); - - validatedIpcMain.once(replyChannel, (_: unknown, data: string) => { - resolve(URI.parse(data)); - }); - - try { - await timeout(5000); - cts.cancel(); - reject(new Error('Timed out waiting for issue reporter URI')); - } finally { - validatedIpcMain.removeHandler(replyChannel); - } - }); - } - - async $getIssueReporterData(extensionId: string): Promise { - const window = this.issueReporterWindowCheck(); - const replyChannel = `vscode:triggerIssueDataProviderResponse${window.id}`; - return Promises.withAsyncBody(async (resolve) => { - - const cts = new CancellationTokenSource(); - window.sendWhenReady('vscode:triggerIssueDataProvider', cts.token, { replyChannel, extensionId }); - - validatedIpcMain.once(replyChannel, (_: unknown, data: string) => { - resolve(data); - }); - - try { - await timeout(5000); - cts.cancel(); - resolve('Error: Extension timed out waiting for issue reporter data'); - } finally { - validatedIpcMain.removeHandler(replyChannel); - } - }); - } - - async $getIssueReporterTemplate(extensionId: string): Promise { - const window = this.issueReporterWindowCheck(); - const replyChannel = `vscode:triggerIssueDataTemplateResponse${window.id}`; - return Promises.withAsyncBody(async (resolve) => { - - const cts = new CancellationTokenSource(); - window.sendWhenReady('vscode:triggerIssueDataTemplate', cts.token, { replyChannel, extensionId }); - - validatedIpcMain.once(replyChannel, (_: unknown, data: string) => { - resolve(data); - }); - - try { - await timeout(5000); - cts.cancel(); - resolve('Error: Extension timed out waiting for issue reporter template'); - } finally { - validatedIpcMain.removeHandler(replyChannel); - } - }); - } - - async $getReporterStatus(extensionId: string, extensionName: string): Promise { - const defaultResult = [false, false]; - const window = this.issueReporterWindowCheck(); - const replyChannel = `vscode:triggerReporterStatus`; - const cts = new CancellationTokenSource(); - window.sendWhenReady(replyChannel, cts.token, { replyChannel, extensionId, extensionName }); - const result = await raceTimeout(new Promise(resolve => validatedIpcMain.once('vscode:triggerReporterStatusResponse', (_: unknown, data: boolean[]) => resolve(data))), 2000, () => { - this.logService.error('Error: Extension timed out waiting for reporter status'); - cts.cancel(); - }); - return (result ?? defaultResult) as boolean[]; - } - - - async $sendReporterMenu(extensionId: string, extensionName: string): Promise { + async $sendReporterMenu(extensionId: string, extensionName: string): Promise { const window = this.issueReporterWindowCheck(); const replyChannel = `vscode:triggerReporterMenu`; const cts = new CancellationTokenSource(); window.sendWhenReady(replyChannel, cts.token, { replyChannel, extensionId, extensionName }); - const result = await raceTimeout(new Promise(resolve => validatedIpcMain.once(`vscode:triggerReporterMenuResponse:${extensionId}`, (_: unknown, data: IssueReporterData | undefined) => resolve(data))), 5000, () => { + const result = await raceTimeout(new Promise(resolve => validatedIpcMain.once(`vscode:triggerReporterMenuResponse:${extensionId}`, (_: unknown, data: OldIssueReporterData | undefined) => resolve(data))), 5000, () => { this.logService.error(`Error: Extension ${extensionId} timed out waiting for menu response`); cts.cancel(); }); - return result as IssueReporterData | undefined; + return result as OldIssueReporterData | undefined; } async $closeReporter(): Promise { this.issueReporterWindow?.close(); } - async closeProcessExplorer(): Promise { - this.processExplorerWindow?.close(); - } - //#endregion private focusWindow(window: BrowserWindow): void { @@ -484,12 +199,6 @@ export class IssueMainService implements IIssueMainService { window.focus(); } - private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void { - if (!event.sender.isDestroyed()) { - event.sender.send(channel, ...args); - } - } - private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions, windowKind: string): BrowserWindow { const window = new BrowserWindow({ fullscreen: false, @@ -590,15 +299,3 @@ export class IssueMainService implements IIssueMainService { return state; } } - -function isStrictWindowState(obj: unknown): obj is IStrictWindowState { - if (typeof obj !== 'object' || obj === null) { - return false; - } - return ( - 'x' in obj && - 'y' in obj && - 'width' in obj && - 'height' in obj - ); -} diff --git a/src/vs/platform/issue/electron-main/processMainService.ts b/src/vs/platform/issue/electron-main/processMainService.ts new file mode 100644 index 00000000000..e6002ae0192 --- /dev/null +++ b/src/vs/platform/issue/electron-main/processMainService.ts @@ -0,0 +1,379 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BrowserWindow, BrowserWindowConstructorOptions, contentTracing, Display, IpcMainEvent, screen } from 'electron'; +import { randomPath } from 'vs/base/common/extpath'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { listProcesses } from 'vs/base/node/ps'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; +import { localize } from 'vs/nls'; +import { IDiagnosticsService, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { IProcessMainService, ProcessExplorerData, ProcessExplorerWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IIPCObjectUrl, IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; +import { IStateService } from 'vs/platform/state/node/state'; +import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; +import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { IWindowState } from 'vs/platform/window/electron-main/window'; + +const processExplorerWindowState = 'issue.processExplorerWindowState'; + +interface IBrowserWindowOptions { + backgroundColor: string | undefined; + title: string; + zoomLevel: number; + alwaysOnTop: boolean; +} + +type IStrictWindowState = Required>; + +export class ProcessMainService implements IProcessMainService { + + declare readonly _serviceBrand: undefined; + + private static readonly DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; + + private processExplorerWindow: BrowserWindow | null = null; + private processExplorerParentWindow: BrowserWindow | null = null; + + constructor( + private userEnv: IProcessEnvironment, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, + @ILogService private readonly logService: ILogService, + @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, + @IDiagnosticsMainService private readonly diagnosticsMainService: IDiagnosticsMainService, + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @IProtocolMainService private readonly protocolMainService: IProtocolMainService, + @IProductService private readonly productService: IProductService, + @IStateService private readonly stateService: IStateService, + ) { + this.registerListeners(); + } + + //#region Register Listeners + + private registerListeners(): void { + validatedIpcMain.on('vscode:listProcesses', async event => { + const processes = []; + + try { + processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(process.pid) }); + + const remoteDiagnostics = await this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true }); + remoteDiagnostics.forEach(data => { + if (isRemoteDiagnosticError(data)) { + processes.push({ + name: data.hostName, + rootProcess: data + }); + } else { + if (data.processes) { + processes.push({ + name: data.hostName, + rootProcess: data.processes + }); + } + } + }); + } catch (e) { + this.logService.error(`Listing processes failed: ${e}`); + } + + this.safeSend(event, 'vscode:listProcessesResponse', processes); + }); + + validatedIpcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { + const { id, from, args } = commandInfo; + + let parentWindow: BrowserWindow | null; + switch (from) { + case 'processExplorer': + parentWindow = this.processExplorerParentWindow; + break; + default: + // The issue reporter does not use this anymore. + throw new Error(`Unexpected command source: ${from}`); + } + + parentWindow?.webContents.send('vscode:runAction', { id, from, args }); + }); + + validatedIpcMain.on('vscode:closeProcessExplorer', event => { + this.processExplorerWindow?.close(); + }); + + validatedIpcMain.on('vscode:pidToNameRequest', async event => { + const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics(); + + const pidToNames: [number, string][] = []; + for (const window of mainProcessInfo.windows) { + pidToNames.push([window.pid, `window [${window.id}] (${window.title})`]); + } + + for (const { pid, name } of UtilityProcess.getAll()) { + pidToNames.push([pid, name]); + } + + this.safeSend(event, 'vscode:pidToNameResponse', pidToNames); + }); + } + + async openProcessExplorer(data: ProcessExplorerData): Promise { + if (!this.processExplorerWindow) { + this.processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this.processExplorerParentWindow) { + const processExplorerDisposables = new DisposableStore(); + + const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); + + const savedPosition = this.stateService.getItem(processExplorerWindowState, undefined); + const position = isStrictWindowState(savedPosition) ? savedPosition : this.getWindowPosition(this.processExplorerParentWindow, 800, 500); + + this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + zoomLevel: data.zoomLevel, + alwaysOnTop: true + }, 'process-explorer'); + + // Store into config object URL + processExplorerWindowConfigUrl.update({ + appRoot: this.environmentMainService.appRoot, + windowId: this.processExplorerWindow.id, + userEnv: this.userEnv, + data, + product, + nls: { + messages: globalThis._VSCODE_NLS_MESSAGES, + language: globalThis._VSCODE_NLS_LANGUAGE + } + }); + + this.processExplorerWindow.loadURL( + FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) + ); + + this.processExplorerWindow.on('close', () => { + this.processExplorerWindow = null; + processExplorerDisposables.dispose(); + }); + + this.processExplorerParentWindow.on('close', () => { + if (this.processExplorerWindow) { + this.processExplorerWindow.close(); + this.processExplorerWindow = null; + + processExplorerDisposables.dispose(); + } + }); + + const storeState = () => { + if (!this.processExplorerWindow) { + return; + } + const size = this.processExplorerWindow.getSize(); + const position = this.processExplorerWindow.getPosition(); + if (!size || !position) { + return; + } + const state: IWindowState = { + width: size[0], + height: size[1], + x: position[0], + y: position[1] + }; + this.stateService.setItem(processExplorerWindowState, state); + }; + + this.processExplorerWindow.on('moved', storeState); + this.processExplorerWindow.on('resized', storeState); + } + } + + if (this.processExplorerWindow) { + this.focusWindow(this.processExplorerWindow); + } + } + + private focusWindow(window: BrowserWindow): void { + if (window.isMinimized()) { + window.restore(); + } + + window.focus(); + } + + private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number): IStrictWindowState { + + // We want the new window to open on the same display that the parent is in + let displayToUse: Display | undefined; + const displays = screen.getAllDisplays(); + + // Single Display + if (displays.length === 1) { + displayToUse = displays[0]; + } + + // Multi Display + else { + + // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is + if (isMacintosh) { + const cursorPoint = screen.getCursorScreenPoint(); + displayToUse = screen.getDisplayNearestPoint(cursorPoint); + } + + // if we have a last active window, use that display for the new window + if (!displayToUse && parentWindow) { + displayToUse = screen.getDisplayMatching(parentWindow.getBounds()); + } + + // fallback to primary display or first display + if (!displayToUse) { + displayToUse = screen.getPrimaryDisplay() || displays[0]; + } + } + + const displayBounds = displayToUse.bounds; + + const state: IStrictWindowState = { + width: defaultWidth, + height: defaultHeight, + x: displayBounds.x + (displayBounds.width / 2) - (defaultWidth / 2), + y: displayBounds.y + (displayBounds.height / 2) - (defaultHeight / 2) + }; + + if (displayBounds.width > 0 && displayBounds.height > 0 /* Linux X11 sessions sometimes report wrong display bounds */) { + if (state.x < displayBounds.x) { + state.x = displayBounds.x; // prevent window from falling out of the screen to the left + } + + if (state.y < displayBounds.y) { + state.y = displayBounds.y; // prevent window from falling out of the screen to the top + } + + if (state.x > (displayBounds.x + displayBounds.width)) { + state.x = displayBounds.x; // prevent window from falling out of the screen to the right + } + + if (state.y > (displayBounds.y + displayBounds.height)) { + state.y = displayBounds.y; // prevent window from falling out of the screen to the bottom + } + + if (state.width > displayBounds.width) { + state.width = displayBounds.width; // prevent window from exceeding display bounds width + } + + if (state.height > displayBounds.height) { + state.height = displayBounds.height; // prevent window from exceeding display bounds height + } + } + + return state; + } + + async stopTracing(): Promise { + if (!this.environmentMainService.args.trace) { + return; // requires tracing to be on + } + + const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); + + // Inform user to report an issue + await this.dialogMainService.showMessageBox({ + type: 'info', + message: localize('trace.message', "Successfully created the trace file"), + detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), + buttons: [localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], + }, BrowserWindow.getFocusedWindow() ?? undefined); + + // Show item in explorer + this.nativeHostMainService.showItemInFolder(undefined, path); + } + + async getSystemStatus(): Promise { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); + return this.diagnosticsService.getDiagnostics(info, remoteData); + } + + async $getSystemInfo(): Promise { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); + const msg = await this.diagnosticsService.getSystemInfo(info, remoteData); + return msg; + } + + async $getPerformanceInfo(): Promise { + try { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]); + return await this.diagnosticsService.getPerformanceInfo(info, remoteData); + } catch (error) { + this.logService.warn('issueService#getPerformanceInfo ', error.message); + + throw error; + } + } + + private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions, windowKind: string): BrowserWindow { + const window = new BrowserWindow({ + fullscreen: false, + skipTaskbar: false, + resizable: true, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + title: options.title, + backgroundColor: options.backgroundColor || ProcessMainService.DEFAULT_BACKGROUND_COLOR, + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath, + additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`], + v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', + enableWebSQL: false, + spellcheck: false, + zoomFactor: zoomLevelToZoomFactor(options.zoomLevel), + sandbox: true + }, + alwaysOnTop: options.alwaysOnTop, + experimentalDarkMode: true + } as BrowserWindowConstructorOptions & { experimentalDarkMode: boolean }); + + window.setMenuBarVisibility(false); + + return window; + } + + private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void { + if (!event.sender.isDestroyed()) { + event.sender.send(channel, ...args); + } + } + + async closeProcessExplorer(): Promise { + this.processExplorerWindow?.close(); + } +} + +function isStrictWindowState(obj: unknown): obj is IStrictWindowState { + if (typeof obj !== 'object' || obj === null) { + return false; + } + return ( + 'x' in obj && + 'y' in obj && + 'width' in obj && + 'height' in obj + ); +} diff --git a/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts index 08322db2bb6..039aaa6cc9b 100644 --- a/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts +++ b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { getCompressedContent, IJSONSchema } from 'vs/base/common/jsonSchema'; import * as platform from 'vs/platform/registry/common/platform'; export const Extensions = { @@ -35,6 +35,18 @@ export interface IJSONContributionRegistry { * Get all schemas */ getSchemaContributions(): ISchemaContributions; + + /** + * Gets the (compressed) content of the schema with the given schema ID (if any) + * @param uri The id of the schema + */ + getSchemaContent(uri: string): string | undefined; + + /** + * Returns true if there's a schema that matches the given schema ID + * @param uri The id of the schema + */ + hasSchemaContent(uri: string): boolean; } @@ -74,6 +86,15 @@ class JSONContributionRegistry implements IJSONContributionRegistry { }; } + public getSchemaContent(uri: string): string | undefined { + const schema = this.schemasById[uri]; + return schema ? getCompressedContent(schema) : undefined; + } + + public hasSchemaContent(uri: string): boolean { + return !!this.schemasById[uri]; + } + } const jsonContributionRegistry = new JSONContributionRegistry(); diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index 4ab820a3732..6c88451dff6 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -43,6 +43,9 @@ export interface IKeybindingRule extends IKeybindings { id: string; weight: number; args?: any; + /** + * Keybinding is disabled if expression returns false. + */ when?: ContextKeyExpression | null | undefined; } diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 2f79c811d5e..88aa86a05d3 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { createSimpleKeybinding, ResolvedKeybinding, KeyCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index dfc5df1e096..b88a747e454 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index 54bd2a6761d..3fccf387540 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { decodeKeybinding, createSimpleKeybinding, KeyCodeChord } from 'vs/base/common/keybindings'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { OS } from 'vs/base/common/platform'; diff --git a/src/vs/platform/languagePacks/node/languagePacks.ts b/src/vs/platform/languagePacks/node/languagePacks.ts index 3a7df771bc2..698b9b56de7 100644 --- a/src/vs/platform/languagePacks/node/languagePacks.ts +++ b/src/vs/platform/languagePacks/node/languagePacks.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { createHash } from 'crypto'; import { equals } from 'vs/base/common/arrays'; import { Queue } from 'vs/base/common/async'; @@ -180,7 +181,7 @@ class LanguagePacksCache extends Disposable { private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise { return this.languagePacksFileLimiter.queue(() => { let result: T | null = null; - return Promises.readFile(this.languagePacksFilePath, 'utf8') + return fs.promises.readFile(this.languagePacksFilePath, 'utf8') .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) .then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) .then(languagePacks => { result = fn(languagePacks); return languagePacks; }) diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 8cc5076eea2..1fcb947305d 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -147,7 +147,7 @@ export class LaunchMainService implements ILaunchMainService { let openNewWindow = false; // Force new window - if (args['new-window'] || args['unity-launch'] || baseConfig.forceProfile || baseConfig.forceTempProfile) { + if (args['new-window'] || baseConfig.forceProfile || baseConfig.forceTempProfile) { openNewWindow = true; } diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index ed098115159..4013b2b548b 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow } from 'electron'; +import electron from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, Promises, timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -294,7 +294,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe this.fireOnWillShutdown(ShutdownReason.QUIT); } }; - app.addListener('before-quit', beforeQuitListener); + electron.app.addListener('before-quit', beforeQuitListener); // window-all-closed: an event that only fires when the last window // was closed. We override this event to be in charge if app.quit() @@ -305,14 +305,14 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // Windows/Linux: we quit when all windows have closed // Mac: we only quit when quit was requested if (this._quitRequested || !isMacintosh) { - app.quit(); + electron.app.quit(); } }; - app.addListener('window-all-closed', windowAllClosedListener); + electron.app.addListener('window-all-closed', windowAllClosedListener); // will-quit: an event that is fired after all windows have been // closed, but before actually quitting. - app.once('will-quit', e => { + electron.app.once('will-quit', e => { this.trace('Lifecycle#app.on(will-quit) - begin'); // Prevent the quit until the shutdown promise was resolved @@ -332,12 +332,12 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // will-quit listener is only installed "once". Also // remove any listener we have that is no longer needed - app.removeListener('before-quit', beforeQuitListener); - app.removeListener('window-all-closed', windowAllClosedListener); + electron.app.removeListener('before-quit', beforeQuitListener); + electron.app.removeListener('window-all-closed', windowAllClosedListener); this.trace('Lifecycle#app.on(will-quit) - calling app.quit()'); - app.quit(); + electron.app.quit(); }); }); } @@ -428,7 +428,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // Window Before Closing: Main -> Renderer const win = assertIsDefined(window.win); - win.on('close', e => { + windowListeners.add(Event.fromNodeEventEmitter(win, 'close')(e => { // The window already acknowledged to be closed const windowId = window.id; @@ -457,10 +457,8 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // No veto, close window now window.close(); }); - }); - - // Window After Closing - win.on('closed', () => { + })); + windowListeners.add(Event.fromNodeEventEmitter(win, 'closed')(() => { this.trace(`Lifecycle#window.on('closed') - window ID ${window.id}`); // update window count @@ -475,13 +473,14 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe if (this.windowCounter === 0 && (!isMacintosh || this._quitRequested)) { this.fireOnWillShutdown(ShutdownReason.QUIT); } - }); + })); } registerAuxWindow(auxWindow: IAuxiliaryWindow): void { const win = assertIsDefined(auxWindow.win); - win.on('close', e => { + const windowListeners = new DisposableStore(); + windowListeners.add(Event.fromNodeEventEmitter(win, 'close')(e => { this.trace(`Lifecycle#auxWindow.on('close') - window ID ${auxWindow.id}`); if (this._quitRequested) { @@ -499,11 +498,12 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe e.preventDefault(); } - }); - - win.on('closed', () => { + })); + windowListeners.add(Event.fromNodeEventEmitter(win, 'closed')(() => { this.trace(`Lifecycle#auxWindow.on('closed') - window ID ${auxWindow.id}`); - }); + + windowListeners.dispose(); + })); } async reload(window: ICodeWindow, cli?: NativeParsedArgs): Promise { @@ -652,7 +652,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // Calling app.quit() will trigger the close handlers of each opened window // and only if no window vetoed the shutdown, we will get the will-quit event this.trace('Lifecycle#quit() - calling app.quit()'); - app.quit(); + electron.app.quit(); }); return this.pendingQuitPromise; @@ -690,16 +690,16 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe const quitListener = () => { if (!this.relaunchHandler?.handleRelaunch(options)) { this.trace('Lifecycle#relaunch() - calling app.relaunch()'); - app.relaunch({ args }); + electron.app.relaunch({ args }); } }; - app.once('quit', quitListener); + electron.app.once('quit', quitListener); // `app.relaunch()` does not quit automatically, so we quit first, // check for vetoes and then relaunch from the `app.on('quit')` event const veto = await this.quit(true /* will restart */); if (veto) { - app.removeListener('quit', quitListener); + electron.app.removeListener('quit', quitListener); } } @@ -727,7 +727,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // to a participant within the window. this is not wanted when we // are asked to kill the application. (async () => { - for (const window of BrowserWindow.getAllWindows()) { + for (const window of electron.BrowserWindow.getAllWindows()) { if (window && !window.isDestroyed()) { let whenWindowClosed: Promise; if (window.webContents && !window.webContents.isDestroyed()) { @@ -744,6 +744,6 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe ]); // Now exit either after 1s or all windows destroyed - app.exit(code); + electron.app.exit(code); } } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 6857531cb66..976b9a85dc3 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -1490,7 +1490,7 @@ configurationRegistry.registerConfiguration({ type: 'number', minimum: 1, default: 7, - markdownDescription: localize('sticky scroll maximum items', "Controls the number of sticky elements displayed in the tree when `#workbench.tree.enableStickyScroll#` is enabled."), + markdownDescription: localize('sticky scroll maximum items', "Controls the number of sticky elements displayed in the tree when {0} is enabled.", '`#workbench.tree.enableStickyScroll#`'), }, [typeNavigationModeSettingKey]: { type: 'string', diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 5294be4aefa..91120aab5b6 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -12,7 +12,13 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IMarker, IMarkerData, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers'; -export const unsupportedSchemas = new Set([Schemas.inMemory, Schemas.vscodeSourceControl, Schemas.walkThrough, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock]); +export const unsupportedSchemas = new Set([ + Schemas.inMemory, + Schemas.vscodeSourceControl, + Schemas.walkThrough, + Schemas.walkThroughSnippet, + Schemas.vscodeChatCodeBlock, +]); class DoubleResourceMap { diff --git a/src/vs/platform/markers/test/common/markerService.test.ts b/src/vs/platform/markers/test/common/markerService.test.ts index d8ebccf746f..1b9916a8adf 100644 --- a/src/vs/platform/markers/test/common/markerService.test.ts +++ b/src/vs/platform/markers/test/common/markerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index f11b38cb19d..d7449e94541 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -25,6 +25,7 @@ import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import { INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable, hasNativeTitlebar } from 'vs/platform/window/common/window'; import { IWindowsCountChangedEvent, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; +import { Disposable } from 'vs/base/common/lifecycle'; const telemetryFrom = 'menu'; @@ -42,7 +43,7 @@ interface IMenuItemWithKeybinding { userSettingsLabel?: string; } -export class Menubar { +export class Menubar extends Disposable { private static readonly lastKnownMenubarStorageKey = 'lastKnownMenubarData'; @@ -78,6 +79,8 @@ export class Menubar { @IProductService private readonly productService: IProductService, @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService ) { + super(); + this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0); this.menuGC = new RunOnceScheduler(() => { this.oldMenus = []; }, 10000); @@ -169,12 +172,12 @@ export class Menubar { private registerListeners(): void { // Keep flag when app quits - this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true); + this._register(this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true)); // Listen to some events from window service to update menu - this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e)); - this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus()); - this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus()); + this._register(this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e))); + this._register(this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus())); + this._register(this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus())); } private get currentEnableMenuBarMnemonics(): boolean { @@ -247,7 +250,7 @@ export class Menubar { } const focusedWindow = BrowserWindow.getFocusedWindow(); - this.noActiveMainWindow = !focusedWindow || !!this.auxiliaryWindowsMainService.getWindowById(focusedWindow.id); + this.noActiveMainWindow = !focusedWindow || !!this.auxiliaryWindowsMainService.getWindowByWebContents(focusedWindow.webContents); this.scheduleUpdateMenu(); } @@ -768,7 +771,7 @@ export class Menubar { // actions via the main window. let activeBrowserWindow = BrowserWindow.getFocusedWindow(); if (activeBrowserWindow) { - const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowById(activeBrowserWindow.id); + const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowByWebContents(activeBrowserWindow.webContents); if (auxiliaryWindowCandidate) { activeBrowserWindow = this.windowsMainService.getWindowById(auxiliaryWindowCandidate.parentId)?.win ?? null; } diff --git a/src/vs/platform/menubar/electron-main/menubarMainService.ts b/src/vs/platform/menubar/electron-main/menubarMainService.ts index c680afa296a..731070e4f50 100644 --- a/src/vs/platform/menubar/electron-main/menubarMainService.ts +++ b/src/vs/platform/menubar/electron-main/menubarMainService.ts @@ -8,6 +8,7 @@ import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle import { ILogService } from 'vs/platform/log/common/log'; import { ICommonMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { Menubar } from 'vs/platform/menubar/electron-main/menubar'; +import { Disposable } from 'vs/base/common/lifecycle'; export const IMenubarMainService = createDecorator('menubarMainService'); @@ -15,24 +16,24 @@ export interface IMenubarMainService extends ICommonMenubarService { readonly _serviceBrand: undefined; } -export class MenubarMainService implements IMenubarMainService { +export class MenubarMainService extends Disposable implements IMenubarMainService { declare readonly _serviceBrand: undefined; - private menubar: Promise; + private readonly menubar = this.installMenuBarAfterWindowOpen(); constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService ) { - this.menubar = this.installMenuBarAfterWindowOpen(); + super(); } private async installMenuBarAfterWindowOpen(): Promise { await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); - return this.instantiationService.createInstance(Menubar); + return this._register(this.instantiationService.createInstance(Menubar)); } async updateMenubar(windowId: number, menus: IMenubarData): Promise { diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 94ae20b32f8..344d45395ed 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -11,6 +11,7 @@ import { ISerializableCommandAction } from 'vs/platform/action/common/action'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; @@ -73,10 +74,12 @@ export interface ICommonNativeHostService { getWindows(options: { includeAuxiliaryWindows: false }): Promise>; getWindowCount(): Promise; getActiveWindowId(): Promise; + getActiveWindowPosition(): Promise; openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; + isFullScreen(options?: INativeHostOptions): Promise; toggleFullScreen(options?: INativeHostOptions): Promise; handleTitleDoubleClick(options?: INativeHostOptions): Promise; @@ -125,7 +128,7 @@ export interface ICommonNativeHostService { showItemInFolder(path: string): Promise; setRepresentedFilename(path: string, options?: INativeHostOptions): Promise; setDocumentEdited(edited: boolean, options?: INativeHostOptions): Promise; - openExternal(url: string): Promise; + openExternal(url: string, defaultApplication?: string): Promise; moveItemToTrash(fullPath: string): Promise; isAdmin(): Promise; @@ -141,6 +144,7 @@ export interface ICommonNativeHostService { hasWSLFeatureInstalled(): Promise; // Process + getProcessId(): Promise; killProcess(pid: number, code: string): Promise; // Clipboard @@ -182,6 +186,8 @@ export interface ICommonNativeHostService { // Connectivity resolveProxy(url: string): Promise; + lookupAuthorization(authInfo: AuthInfo): Promise; + lookupKerberosAuthorization(url: string): Promise; loadCertificates(): Promise; findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise; diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/platform/native/electron-main/auth.ts similarity index 54% rename from src/vs/code/electron-main/auth.ts rename to src/vs/platform/native/electron-main/auth.ts index bf2a99f601c..52b1f51c05e 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/platform/native/electron-main/auth.ts @@ -3,14 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, AuthenticationResponseDetails, AuthInfo, Event as ElectronEvent, WebContents } from 'electron'; +import { app, AuthenticationResponseDetails, AuthInfo as ElectronAuthInfo, Event as ElectronEvent, WebContents } from 'electron'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IApplicationStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; @@ -20,55 +25,37 @@ interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDe } type LoginEvent = { - event: ElectronEvent; + event?: ElectronEvent; authInfo: AuthInfo; - req: ElectronAuthenticationResponseDetails; - - callback: (username?: string, password?: string) => void; + callback?: (username?: string, password?: string) => void; }; -type Credentials = { - username: string; - password: string; -}; +export const IProxyAuthService = createDecorator('proxyAuthService'); -enum ProxyAuthState { - - /** - * Initial state: we will try to use stored credentials - * first to reply to the auth challenge. - */ - Initial = 1, - - /** - * We used stored credentials and are still challenged, - * so we will show a login dialog next. - */ - StoredCredentialsUsed, - - /** - * Finally, if we showed a login dialog already, we will - * not show any more login dialogs until restart to reduce - * the UI noise. - */ - LoginDialogShown +export interface IProxyAuthService { + lookupAuthorization(authInfo: AuthInfo): Promise; } -export class ProxyAuthHandler extends Disposable { +export class ProxyAuthService extends Disposable implements IProxyAuthService { + + declare readonly _serviceBrand: undefined; private readonly PROXY_CREDENTIALS_SERVICE_KEY = 'proxy-credentials://'; - private pendingProxyResolve: Promise | undefined = undefined; + private pendingProxyResolves = new Map>(); + private currentDialog: Promise | undefined = undefined; - private state = ProxyAuthState.Initial; + private cancelledAuthInfoHashes = new Set(); - private sessionCredentials: Credentials | undefined = undefined; + private sessionCredentials = new Map(); constructor( @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService, - @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService + @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, ) { super(); @@ -76,39 +63,45 @@ export class ProxyAuthHandler extends Disposable { } private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); + const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, _webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: ElectronAuthInfo, callback) => ({ event, authInfo: { ...authInfo, attempt: req.firstAuthAttempt ? 1 : 2 }, callback } satisfies LoginEvent)); this._register(onLogin(this.onLogin, this)); } - private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { + async lookupAuthorization(authInfo: AuthInfo): Promise { + return this.onLogin({ authInfo }); + } + + private async onLogin({ event, authInfo, callback }: LoginEvent): Promise { if (!authInfo.isProxy) { return; // only for proxy } - if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { - this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); - - return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) - } - // Signal we handle this event on our own, otherwise // Electron will ignore our provided credentials. - event.preventDefault(); + event?.preventDefault(); + + // Compute a hash over the authentication info to be used + // with the credentials store to return the right credentials + // given the properties of the auth request + // (see https://github.com/microsoft/vscode/issues/109497) + const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); let credentials: Credentials | undefined = undefined; - if (!this.pendingProxyResolve) { + let pendingProxyResolve = this.pendingProxyResolves.get(authInfoHash); + if (!pendingProxyResolve) { this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); + pendingProxyResolve = this.resolveProxyCredentials(authInfo, authInfoHash); + this.pendingProxyResolves.set(authInfoHash, pendingProxyResolve); try { - credentials = await this.pendingProxyResolve; + credentials = await pendingProxyResolve; } finally { - this.pendingProxyResolve = undefined; + this.pendingProxyResolves.delete(authInfoHash); } } else { this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); - credentials = await this.pendingProxyResolve; + credentials = await pendingProxyResolve; } // According to Electron docs, it is fine to call back without @@ -118,14 +111,15 @@ export class ProxyAuthHandler extends Disposable { // > If `callback` is called without a username or password, the authentication // > request will be cancelled and the authentication error will be returned to the // > page. - callback(credentials?.username, credentials?.password); + callback?.(credentials?.username, credentials?.password); + return credentials; } - private async resolveProxyCredentials(authInfo: AuthInfo): Promise { + private async resolveProxyCredentials(authInfo: AuthInfo, authInfoHash: string): Promise { this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); try { - const credentials = await this.doResolveProxyCredentials(authInfo); + const credentials = await this.doResolveProxyCredentials(authInfo, authInfoHash); if (credentials) { this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); @@ -140,14 +134,69 @@ export class ProxyAuthHandler extends Disposable { return undefined; } - private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { + private async doResolveProxyCredentials(authInfo: AuthInfo, authInfoHash: string): Promise { this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); - // Compute a hash over the authentication info to be used - // with the credentials store to return the right credentials - // given the properties of the auth request - // (see https://github.com/microsoft/vscode/issues/109497) - const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); + // For testing. + if (this.environmentMainService.extensionTestsLocationURI) { + const credentials = this.configurationService.getValue('integration-test.http.proxyAuth'); + if (credentials) { + const j = credentials.indexOf(':'); + if (j !== -1) { + return { + username: credentials.substring(0, j), + password: credentials.substring(j + 1) + }; + } else { + return { + username: credentials, + password: '' + }; + } + } + return undefined; + } + + // Reply with manually supplied credentials. Fail if they are wrong. + const newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() + || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() + || undefined; + + if (newHttpProxy?.indexOf('@') !== -1) { + const uri = URI.parse(newHttpProxy!); + const i = uri.authority.indexOf('@'); + if (i !== -1) { + if (authInfo.attempt > 1) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - ignoring previously used config/envvar credentials'); + return undefined; // We tried already, let the user handle it. + } + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found config/envvar credentials to use'); + const credentials = uri.authority.substring(0, i); + const j = credentials.indexOf(':'); + if (j !== -1) { + return { + username: credentials.substring(0, j), + password: credentials.substring(j + 1) + }; + } else { + return { + username: credentials, + password: '' + }; + } + } + } + + // Reply with session credentials unless we used them already. + // In that case we need to show a login dialog again because + // they seem invalid. + const sessionCredentials = authInfo.attempt === 1 && this.sessionCredentials.get(authInfoHash); + if (sessionCredentials) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found session credentials to use'); + + const { username, password } = sessionCredentials; + return { username, password }; + } let storedUsername: string | undefined; let storedPassword: string | undefined; @@ -166,13 +215,32 @@ export class ProxyAuthHandler extends Disposable { // Reply with stored credentials unless we used them already. // In that case we need to show a login dialog again because // they seem invalid. - if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { + if (authInfo.attempt === 1 && typeof storedUsername === 'string' && typeof storedPassword === 'string') { this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); - this.state = ProxyAuthState.StoredCredentialsUsed; + this.sessionCredentials.set(authInfoHash, { username: storedUsername, password: storedPassword }); return { username: storedUsername, password: storedPassword }; } + const previousDialog = this.currentDialog; + const currentDialog = this.currentDialog = (async () => { + await previousDialog; + const credentials = await this.showProxyCredentialsDialog(authInfo, authInfoHash, storedUsername, storedPassword); + if (this.currentDialog === currentDialog!) { + this.currentDialog = undefined; + } + return credentials; + })(); + return currentDialog; + } + + private async showProxyCredentialsDialog(authInfo: AuthInfo, authInfoHash: string, storedUsername: string | undefined, storedPassword: string | undefined): Promise { + if (this.cancelledAuthInfoHashes.has(authInfoHash)) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - login dialog was cancelled before, not showing again'); + + return undefined; + } + // Find suitable window to show dialog: prefer to show it in the // active window because any other network request will wait on // the credentials and we want the user to present the dialog. @@ -186,14 +254,14 @@ export class ProxyAuthHandler extends Disposable { this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); // Open proxy dialog + const sessionCredentials = this.sessionCredentials.get(authInfoHash); const payload = { authInfo, - username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored - password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored + username: sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored + password: sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` }; window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); - this.state = ProxyAuthState.LoginDialogShown; // Handle reply const loginDialogCredentials = await new Promise(resolve => { @@ -229,6 +297,7 @@ export class ProxyAuthHandler extends Disposable { // We did not get any credentials from the window (e.g. cancelled) else { + this.cancelledAuthInfoHashes.add(authInfoHash); resolve(undefined); } } @@ -240,7 +309,7 @@ export class ProxyAuthHandler extends Disposable { // Remember credentials for the session in case // the credentials are wrong and we show the dialog // again - this.sessionCredentials = loginDialogCredentials; + this.sessionCredentials.set(authInfoHash, loginDialogCredentials); return loginDialogCredentials; } diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index a1d28301611..6d1f9cc240b 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { exec } from 'child_process'; -import { app, BrowserWindow, clipboard, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell } from 'electron'; +import { app, BrowserWindow, clipboard, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron'; import { arch, cpus, freemem, loadavg, platform, release, totalmem, type } from 'os'; import { promisify } from 'util'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { dirname, join, resolve } from 'vs/base/common/path'; +import { matchesSomeScheme, Schemas } from 'vs/base/common/network'; +import { dirname, join, posix, resolve, win32 } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -43,6 +44,9 @@ import { IV8Profile } from 'vs/platform/profiling/common/profiling'; import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow'; import { CancellationError } from 'vs/base/common/errors'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProxyAuthService } from 'vs/platform/native/electron-main/auth'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -61,7 +65,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @ILogService private readonly logService: ILogService, @IProductService private readonly productService: IProductService, @IThemeMainService private readonly themeMainService: IThemeMainService, - @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IRequestService private readonly requestService: IRequestService, + @IProxyAuthService private readonly proxyAuthService: IProxyAuthService ) { super(); } @@ -79,17 +86,17 @@ export class NativeHostMainService extends Disposable implements INativeHostMain readonly onDidOpenMainWindow = Event.map(this.windowsMainService.onDidOpenWindow, window => window.id); readonly onDidTriggerWindowSystemContextMenu = Event.any( - Event.filter(Event.map(this.windowsMainService.onDidTriggerSystemContextMenu, ({ window, x, y }) => { return { windowId: window.id, x, y }; }), ({ windowId }) => !!this.windowsMainService.getWindowById(windowId)), - Event.filter(Event.map(this.auxiliaryWindowsMainService.onDidTriggerSystemContextMenu, ({ window, x, y }) => { return { windowId: window.id, x, y }; }), ({ windowId }) => !!this.auxiliaryWindowsMainService.getWindowById(windowId)) + Event.map(this.windowsMainService.onDidTriggerSystemContextMenu, ({ window, x, y }) => ({ windowId: window.id, x, y })), + Event.map(this.auxiliaryWindowsMainService.onDidTriggerSystemContextMenu, ({ window, x, y }) => ({ windowId: window.id, x, y })) ); readonly onDidMaximizeWindow = Event.any( - Event.filter(Event.map(this.windowsMainService.onDidMaximizeWindow, window => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)), - Event.filter(Event.map(this.auxiliaryWindowsMainService.onDidMaximizeWindow, window => window.id), windowId => !!this.auxiliaryWindowsMainService.getWindowById(windowId)) + Event.map(this.windowsMainService.onDidMaximizeWindow, window => window.id), + Event.map(this.auxiliaryWindowsMainService.onDidMaximizeWindow, window => window.id) ); readonly onDidUnmaximizeWindow = Event.any( - Event.filter(Event.map(this.windowsMainService.onDidUnmaximizeWindow, window => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)), - Event.filter(Event.map(this.auxiliaryWindowsMainService.onDidUnmaximizeWindow, window => window.id), windowId => !!this.auxiliaryWindowsMainService.getWindowById(windowId)) + Event.map(this.windowsMainService.onDidUnmaximizeWindow, window => window.id), + Event.map(this.auxiliaryWindowsMainService.onDidUnmaximizeWindow, window => window.id) ); readonly onDidChangeWindowFullScreen = Event.any( @@ -105,11 +112,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain readonly onDidBlurMainOrAuxiliaryWindow = Event.any( this.onDidBlurMainWindow, - Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-blur', (event, window: BrowserWindow) => window.id), windowId => !!this.auxiliaryWindowsMainService.getWindowById(windowId)) + Event.map(Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-blur', (event, window: BrowserWindow) => this.auxiliaryWindowsMainService.getWindowByWebContents(window.webContents)), window => !!window), window => window!.id) ); readonly onDidFocusMainOrAuxiliaryWindow = Event.any( this.onDidFocusMainWindow, - Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-focus', (event, window: BrowserWindow) => window.id), windowId => !!this.auxiliaryWindowsMainService.getWindowById(windowId)) + Event.map(Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-focus', (event, window: BrowserWindow) => this.auxiliaryWindowsMainService.getWindowByWebContents(window.webContents)), window => !!window), window => window!.id) ); readonly onDidResumeOS = Event.fromNodeEventEmitter(powerMonitor, 'resume'); @@ -172,6 +179,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return undefined; } + async getActiveWindowPosition(): Promise { + const activeWindow = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + if (activeWindow) { + return activeWindow.getBounds(); + } + return undefined; + } + openWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise; openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { @@ -212,6 +227,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain }, options); } + async isFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + return window?.isFullScreen ?? false; + } + async toggleFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); window?.toggleFullScreen(); @@ -317,7 +337,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } // Different source, delete it first - await Promises.unlink(source); + await fs.promises.unlink(source); } catch (error) { if (error.code !== 'ENOENT') { throw error; // throw on any error but file not found @@ -325,7 +345,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } try { - await Promises.symlink(target, source); + await fs.promises.symlink(target, source); } catch (error) { if (error.code !== 'EACCES' && error.code !== 'ENOENT') { throw error; @@ -357,7 +377,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const { source } = await this.getShellCommandLink(); try { - await Promises.unlink(source); + await fs.promises.unlink(source); } catch (error) { switch (error.code) { case 'EACCES': { @@ -480,14 +500,55 @@ export class NativeHostMainService extends Disposable implements INativeHostMain window?.setDocumentEdited(edited); } - async openExternal(windowId: number | undefined, url: string): Promise { + async openExternal(windowId: number | undefined, url: string, defaultApplication?: string): Promise { this.environmentMainService.unsetSnapExportedVariables(); - shell.openExternal(url); - this.environmentMainService.restoreSnapExportedVariables(); + try { + if (matchesSomeScheme(url, Schemas.http, Schemas.https)) { + this.openExternalBrowser(url, defaultApplication); + } else { + shell.openExternal(url); + } + } finally { + this.environmentMainService.restoreSnapExportedVariables(); + } return true; } + private async openExternalBrowser(url: string, defaultApplication?: string) { + const configuredBrowser = defaultApplication ?? this.configurationService.getValue('workbench.externalBrowser'); + if (!configuredBrowser) { + return shell.openExternal(url); + } + + if (configuredBrowser.includes(posix.sep) || configuredBrowser.includes(win32.sep)) { + const browserPathExists = await Promises.exists(configuredBrowser); + if (!browserPathExists) { + this.logService.error(`Configured external browser path does not exist: ${configuredBrowser}`); + return shell.openExternal(url); + } + } + + try { + const { default: open } = await import('open'); + const res = await open(url, { + app: { + // Use `open.apps` helper to allow cross-platform browser + // aliases to be looked up properly. Fallback to the + // configured value if not found. + name: Object.hasOwn(open.apps, configuredBrowser) ? open.apps[(configuredBrowser as keyof typeof open['apps'])] : configuredBrowser + } + }); + + res.stderr?.on('data', (data: Buffer) => { + this.logService.error(`Error openening external URL '${url}' using browser '${configuredBrowser}': ${data.toString()}`); + }); + } catch (error) { + this.logService.error(`Unable to open external URL '${url}' using browser '${configuredBrowser}' due to ${error}.`); + return shell.openExternal(url); + } + } + moveItemToTrash(windowId: number | undefined, fullPath: string): Promise { return shell.trashItem(fullPath); } @@ -495,7 +556,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async isAdmin(): Promise { let isAdmin: boolean; if (isWindows) { - isAdmin = (await import('native-is-elevated'))(); + isAdmin = (await import('native-is-elevated')).default(); } else { isAdmin = process.getuid?.() === 0; } @@ -610,6 +671,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Process + async getProcessId(windowId: number | undefined): Promise { + const window = this.windowById(undefined, windowId); + return window?.win?.webContents.getOSProcessId(); + } + async killProcess(windowId: number | undefined, pid: number, code: string): Promise { process.kill(pid, code); } @@ -755,15 +821,28 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Connectivity async resolveProxy(windowId: number | undefined, url: string): Promise { + if (this.environmentMainService.extensionTestsLocationURI) { + const testProxy = this.configurationService.getValue('integration-test.http.proxy'); + if (testProxy) { + return testProxy; + } + } const window = this.codeWindowById(windowId); const session = window?.win?.webContents?.session; return session?.resolveProxy(url); } + async lookupAuthorization(_windowId: number | undefined, authInfo: AuthInfo): Promise { + return this.proxyAuthService.lookupAuthorization(authInfo); + } + + async lookupKerberosAuthorization(_windowId: number | undefined, url: string): Promise { + return this.requestService.lookupKerberosAuthorization(url); + } + async loadCertificates(_windowId: number | undefined): Promise { - const proxyAgent = await import('@vscode/proxy-agent'); - return proxyAgent.loadSystemCertificates({ log: this.logService }); + return this.requestService.loadCertificates(); } findFreePort(windowId: number | undefined, startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise { @@ -836,6 +915,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return undefined; } - return this.auxiliaryWindowsMainService.getWindowById(windowId); + const contents = webContents.fromId(windowId); + if (!contents) { + return undefined; + } + + return this.auxiliaryWindowsMainService.getWindowByWebContents(contents); } } diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts new file mode 100644 index 00000000000..2e886ef6540 --- /dev/null +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/lifecycle'; +import { autorunOpts, IObservable, IReader } from 'vs/base/common/observable'; +import { observableFromEventOpts } from 'vs/base/common/observableInternal/utils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyValue, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +/** Creates an observable update when a configuration key updates. */ +export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEventOpts({ debugName: () => `Configuration Key "${key}"`, }, + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue + ); +} + +/** Update the configuration key with a value derived from observables. */ +export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { + const boundKey = key.bindTo(service); + return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { + boundKey.set(computeValue(reader)); + }); +} + diff --git a/src/vs/platform/observable/common/wrapInReloadableClass.ts b/src/vs/platform/observable/common/wrapInReloadableClass.ts new file mode 100644 index 00000000000..cfecf902c5f --- /dev/null +++ b/src/vs/platform/observable/common/wrapInReloadableClass.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { isHotReloadEnabled } from 'vs/base/common/hotReload'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { autorunWithStore } from 'vs/base/common/observable'; +import { BrandedService, GetLeadingNonServiceArgs, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +/** + * Wrap a class in a reloadable wrapper. + * When the wrapper is created, the original class is created. + * When the original class changes, the instance is re-created. +*/ +export function wrapInReloadableClass0(getClass: () => Result): Result> { + return !isHotReloadEnabled() ? getClass() : createWrapper(getClass, BaseClass0); +} + +type Result = new (...args: TArgs) => IDisposable; + +class BaseClass { + constructor( + public readonly instantiationService: IInstantiationService, + ) { } + + public init(...params: any[]): void { } +} + +function createWrapper(getClass: () => any, B: new (...args: T) => BaseClass) { + return (class ReloadableWrapper extends B { + private _autorun: IDisposable | undefined = undefined; + + override init(...params: any[]) { + this._autorun = autorunWithStore((reader, store) => { + const clazz = readHotReloadableExport(getClass(), reader); + store.add(this.instantiationService.createInstance(clazz as any, ...params) as IDisposable); + }); + } + + dispose(): void { + this._autorun?.dispose(); + } + }) as any; +} + +class BaseClass0 extends BaseClass { + constructor(@IInstantiationService i: IInstantiationService) { super(i); this.init(); } +} + +/** + * Wrap a class in a reloadable wrapper. + * When the wrapper is created, the original class is created. + * When the original class changes, the instance is re-created. +*/ +export function wrapInReloadableClass1(getClass: () => Result): Result> { + return !isHotReloadEnabled() ? getClass() as any : createWrapper(getClass, BaseClass1); +} + +class BaseClass1 extends BaseClass { + constructor(param1: any, @IInstantiationService i: IInstantiationService,) { super(i); this.init(param1); } +} diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 147c49f2da7..eb93b66a122 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -12,9 +12,10 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import 'vs/css!./link'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export interface ILinkDescriptor { readonly label: string | HTMLElement; @@ -32,7 +33,7 @@ export interface ILinkOptions { export class Link extends Disposable { private el: HTMLAnchorElement; - private hover?: ICustomHover; + private hover?: IManagedHover; private hoverDelegate: IHoverDelegate; private _enabled: boolean = true; @@ -84,6 +85,7 @@ export class Link extends Disposable { container: HTMLElement, private _link: ILinkDescriptor, options: ILinkOptions = {}, + @IHoverService private readonly _hoverService: IHoverService, @IOpenerService openerService: IOpenerService ) { super(); @@ -129,7 +131,7 @@ export class Link extends Disposable { if (this.hoverDelegate.showNativeHover) { this.el.title = title ?? ''; } else if (!this.hover && title) { - this.hover = this._register(setupCustomHover(this.hoverDelegate, this.el, title)); + this.hover = this._register(this._hoverService.setupManagedHover(this.hoverDelegate, this.el, title)); } else if (this.hover) { this.hover.update(title); } diff --git a/src/vs/platform/opener/test/common/nullOpenerService.ts b/src/vs/platform/opener/test/common/nullOpenerService.ts index 25b807c1dce..e5d5a3a0540 100644 --- a/src/vs/platform/opener/test/common/nullOpenerService.ts +++ b/src/vs/platform/opener/test/common/nullOpenerService.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IOpenerService } from '../../common/opener'; -export const NullOpenerService = Object.freeze({ +export const NullOpenerService = Object.freeze({ _serviceBrand: undefined, registerOpener() { return Disposable.None; }, registerValidator() { return Disposable.None; }, @@ -16,4 +16,4 @@ export const NullOpenerService = Object.freeze({ registerExternalOpener() { return Disposable.None; }, async open() { return false; }, async resolveExternalUri(uri: URI) { return { resolved: uri, dispose() { } }; }, -} as IOpenerService); +}); diff --git a/src/vs/platform/opener/test/common/opener.test.ts b/src/vs/platform/opener/test/common/opener.test.ts index 35b6e027d66..93ee50f9901 100644 --- a/src/vs/platform/opener/test/common/opener.test.ts +++ b/src/vs/platform/opener/test/common/opener.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { extractSelection, withSelection } from 'vs/platform/opener/common/opener'; diff --git a/src/vs/platform/policy/node/nativePolicyService.ts b/src/vs/platform/policy/node/nativePolicyService.ts index d6e3f60cdd1..747177ae7e9 100644 --- a/src/vs/platform/policy/node/nativePolicyService.ts +++ b/src/vs/platform/policy/node/nativePolicyService.ts @@ -13,7 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log'; export class NativePolicyService extends AbstractPolicyService implements IPolicyService { private throttler = new Throttler(); - private watcher = this._register(new MutableDisposable()); + private readonly watcher = this._register(new MutableDisposable()); constructor( @ILogService private readonly logService: ILogService, diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 467e11cc56a..58278d978f9 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -53,12 +53,12 @@ else if (globalThis._VSCODE_PRODUCT_JSON && globalThis._VSCODE_PACKAGE_JSON) { else { // Built time configuration (do NOT modify) - product = { /*BUILD->INSERT_PRODUCT_CONFIGURATION*/ } as IProductConfiguration; + product = { /*BUILD->INSERT_PRODUCT_CONFIGURATION*/ } as any; // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.87.0-dev', + version: '1.91.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', diff --git a/src/vs/platform/profiling/common/profiling.ts b/src/vs/platform/profiling/common/profiling.ts index c4bfe9ddd24..f4b4a302750 100644 --- a/src/vs/platform/profiling/common/profiling.ts +++ b/src/vs/platform/profiling/common/profiling.ts @@ -37,7 +37,7 @@ export interface IV8InspectProfilingService { _serviceBrand: undefined; - startProfiling(options: { port: number }): Promise; + startProfiling(options: { host: string; port: number }): Promise; stopProfiling(sessionId: string): Promise; } diff --git a/src/vs/platform/profiling/common/profilingTelemetrySpec.ts b/src/vs/platform/profiling/common/profilingTelemetrySpec.ts index cd85a0d7385..316b6321f34 100644 --- a/src/vs/platform/profiling/common/profilingTelemetrySpec.ts +++ b/src/vs/platform/profiling/common/profilingTelemetrySpec.ts @@ -22,10 +22,10 @@ type TelemetrySampleData = { type TelemetrySampleDataClassification = { owner: 'jrieken'; comment: 'A callstack that took a long time to execute'; - selfTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Self time of the sample' }; - totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Total time of the sample' }; - percentage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Relative time (percentage) of the sample' }; - perfBaseline: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Performance baseline for the machine' }; + selfTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Self time of the sample' }; + totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total time of the sample' }; + percentage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Relative time (percentage) of the sample' }; + perfBaseline: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Performance baseline for the machine' }; functionName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the sample' }; callers: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The heaviest call trace into this sample' }; callersAnnotated: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The heaviest call trace into this sample annotated with respective costs' }; diff --git a/src/vs/platform/profiling/node/profilingService.ts b/src/vs/platform/profiling/node/profilingService.ts index 6187edd76c3..c3f218d047c 100644 --- a/src/vs/platform/profiling/node/profilingService.ts +++ b/src/vs/platform/profiling/node/profilingService.ts @@ -13,9 +13,9 @@ export class InspectProfilingService implements IV8InspectProfilingService { private readonly _sessions = new Map(); - async startProfiling(options: { port: number }): Promise { + async startProfiling(options: { host: string; port: number }): Promise { const prof = await import('v8-inspect-profiler'); - const session = await prof.startProfiling({ port: options.port, checkForPaused: true }); + const session = await prof.startProfiling({ host: options.host, port: options.port, checkForPaused: true }); const id = generateUuid(); this._sessions.set(id, session); return id; diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index e1eb0a2f5f2..2a1c7349aae 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -65,7 +65,7 @@ export interface IProgressNotificationOptions extends IProgressOptions { readonly secondaryActions?: readonly IAction[]; readonly delay?: number; readonly priority?: NotificationPriority; - readonly type?: 'syncing' | 'loading'; + readonly type?: 'loading' | 'syncing'; } export interface IProgressDialogOptions extends IProgressOptions { @@ -77,7 +77,7 @@ export interface IProgressDialogOptions extends IProgressOptions { export interface IProgressWindowOptions extends IProgressOptions { readonly location: ProgressLocation.Window; readonly command?: string; - readonly type?: 'syncing' | 'loading'; + readonly type?: 'loading' | 'syncing'; } export interface IProgressCompositeOptions extends IProgressOptions { diff --git a/src/vs/platform/progress/test/common/progress.test.ts b/src/vs/platform/progress/test/common/progress.test.ts index 85bce306781..24c2ddb78de 100644 --- a/src/vs/platform/progress/test/common/progress.test.ts +++ b/src/vs/platform/progress/test/common/progress.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AsyncProgress } from 'vs/platform/progress/common/progress'; diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index 2b0a52627a8..8b6fd7b5868 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -24,7 +24,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ declare readonly _serviceBrand: undefined; private readonly validRoots = TernarySearchTree.forPaths(!isLinux); - private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']); // https://github.com/microsoft/vscode/issues/119384 + private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.mp4']); // https://github.com/microsoft/vscode/issues/119384 constructor( @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 7aabb2705dd..feb47bd2f62 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -28,6 +28,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export interface ICommandQuickPick extends IPickerQuickAccessItem { readonly commandId: string; + readonly commandWhen?: string; readonly commandAlias?: string; readonly commandDescription?: ILocalizedString; tfIdfScore?: number; @@ -55,7 +56,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc constructor( options: ICommandsQuickAccessOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService private readonly keybindingService: IKeybindingService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IDialogService private readonly dialogService: IDialogService diff --git a/src/vs/platform/quickinput/browser/helpQuickAccess.ts b/src/vs/platform/quickinput/browser/helpQuickAccess.ts index 0346fc0b856..0a93158786c 100644 --- a/src/vs/platform/quickinput/browser/helpQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/helpQuickAccess.ts @@ -25,7 +25,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { @IKeybindingService private readonly keybindingService: IKeybindingService ) { } - provide(picker: IQuickPick): IDisposable { + provide(picker: IQuickPick): IDisposable { const disposables = new DisposableStore(); // Open a picker with the selected value if picked diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 3afa06e5888..9108ee9ae80 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -16,7 +16,8 @@ .quick-input-titlebar { display: flex; align-items: center; - border-radius: inherit; + border-top-right-radius: 5px; + border-top-left-radius: 5px; } .quick-input-left-action-bar { @@ -25,6 +26,10 @@ flex: 1; } +.quick-input-inline-action-bar { + margin: 2px 0 0 5px; +} + .quick-input-title { padding: 3px 0px; text-align: center; @@ -172,7 +177,6 @@ box-sizing: border-box; overflow: hidden; display: flex; - height: 100%; padding: 0 6px; } @@ -295,7 +299,7 @@ .quick-input-list .quick-input-list-entry-action-bar .action-label.codicon { margin-right: 4px; - padding: 0px 2px 2px 2px; + padding: 2px; } .quick-input-list .quick-input-list-entry-action-bar { diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 86160c9e26a..eded3510549 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -133,7 +133,7 @@ export abstract class PickerQuickAccessProvider, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + provide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // Apply options if any @@ -326,6 +326,14 @@ export abstract class PickerQuickAccessProvider { + if (runOptions?.handleAccept) { + if (!event.inBackground) { + picker.hide(); // hide picker unless we accept in background + } + runOptions.handleAccept?.(picker.activeItems[0]); + return; + } + const [item] = picker.selectedItems; if (typeof item?.accept === 'function') { if (!event.inBackground) { diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 88169c32ed5..b66289c83c2 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -8,7 +8,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessProviderRunOptions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; +import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService, IQuickPick, IQuickPickItem, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -20,7 +20,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon private readonly lastAcceptedPickerValues = new Map(); private visibleQuickAccess: { - readonly picker: IQuickPick; + readonly picker: IQuickPick; readonly descriptor: IQuickAccessProviderDescriptor | undefined; readonly value: string; } | undefined = undefined; @@ -45,7 +45,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon private doShowOrPick(value: string, pick: boolean, options?: IQuickAccessOptions): Promise | void { // Find provider for the value to show - const [provider, descriptor] = this.getOrInstantiateProvider(value); + const [provider, descriptor] = this.getOrInstantiateProvider(value, options?.enabledProviderPrefixes); // Return early if quick access is already showing on that same prefix const visibleQuickAccess = this.visibleQuickAccess; @@ -99,10 +99,10 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Create a picker for the provider to use with the initial value // and adjust the filtering to exclude the prefix from filtering const disposables = new DisposableStore(); - const picker = disposables.add(this.quickInputService.createQuickPick()); + const picker = disposables.add(this.quickInputService.createQuickPick({ useSeparators: true })); picker.value = value; this.adjustValueSelection(picker, descriptor, options); - picker.placeholder = descriptor?.placeholder; + picker.placeholder = options?.placeholder ?? descriptor?.placeholder; picker.quickNavigate = options?.quickNavigateConfiguration; picker.hideInput = !!picker.quickNavigate && !visibleQuickAccess; // only hide input if there was no picker opened already if (typeof options?.itemActivation === 'number' || options?.quickNavigateConfiguration) { @@ -123,7 +123,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } // Register listeners - disposables.add(this.registerPickerListeners(picker, provider, descriptor, value, options?.providerOptions)); + disposables.add(this.registerPickerListeners(picker, provider, descriptor, value, options)); // Ask provider to fill the picker as needed if we have one // and pass over a cancellation token that will indicate when @@ -163,7 +163,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } } - private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void { + private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void { let valueSelection: [number, number]; // Preserve: just always put the cursor at the end @@ -180,11 +180,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } private registerPickerListeners( - picker: IQuickPick, + picker: IQuickPick, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string, - providerOptions?: IQuickAccessProviderRunOptions + options?: IQuickAccessOptions ): IDisposable { const disposables = new DisposableStore(); @@ -199,13 +199,14 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Whenever the value changes, check if the provider has // changed and if so - re-create the picker from the beginning disposables.add(picker.onDidChangeValue(value => { - const [providerForValue] = this.getOrInstantiateProvider(value); + const [providerForValue] = this.getOrInstantiateProvider(value, options?.enabledProviderPrefixes); if (providerForValue !== provider) { this.show(value, { + enabledProviderPrefixes: options?.enabledProviderPrefixes, // do not rewrite value from user typing! preserveValue: true, // persist the value of the providerOptions from the original showing - providerOptions + providerOptions: options?.providerOptions }); } else { visibleQuickAccess.value = value; // remember the value in our visible one @@ -222,9 +223,9 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon return disposables; } - private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { + private getOrInstantiateProvider(value: string, enabledProviderPrefixes?: string[]): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { const providerDescriptor = this.registry.getQuickAccessProvider(value); - if (!providerDescriptor) { + if (!providerDescriptor || enabledProviderPrefixes && !enabledProviderPrefixes?.includes(providerDescriptor.prefix)) { return [undefined, undefined]; } diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 0cce0ef4228..afc73c9ec35 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -17,20 +17,33 @@ import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { equals } from 'vs/base/common/arrays'; import { TimeoutTimer } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event, EventBufferer } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { isIOS, isMacintosh } from 'vs/base/common/platform'; +import { isIOS } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./media/quickInput'; import { localize } from 'vs/nls'; -import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputButtonLocation, QuickInputHideReason, QuickInputType, QuickPickFocus } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from './quickInputBox'; import { quickInputButtonToAction, renderQuickInputDescription } from './quickInputUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IHoverOptions, IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; -import { QuickInputListFocus, QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; +import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; +import type { IHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const inQuickInputContextKeyValue = 'inQuickInput'; +export const InQuickInputContextKey = new RawContextKey(inQuickInputContextKeyValue, false, localize('inQuickInput', "Whether keyboard focus is inside the quick input control")); +export const inQuickInputContext = ContextKeyExpr.has(inQuickInputContextKeyValue); + +export const quickInputTypeContextKeyValue = 'quickInputType'; +export const QuickInputTypeContextKey = new RawContextKey(quickInputTypeContextKeyValue, undefined, localize('quickInputType', "The type of the currently visible quick input")); + +export const endOfQuickInputBoxContextKeyValue = 'cursorAtEndOfQuickInputBox'; +export const EndOfQuickInputBoxContextKey = new RawContextKey(endOfQuickInputBoxContextKeyValue, false, localize('cursorAtEndOfQuickInputBox', "Whether the cursor in the quick input is at the end of the input box")); +export const endOfQuickInputBoxContext = ContextKeyExpr.has(endOfQuickInputBoxContextKeyValue); export interface IQuickInputOptions { idPrefix: string; @@ -86,6 +99,7 @@ export interface QuickInputUI { description2: HTMLElement; widget: HTMLElement; rightActionBar: ActionBar; + inlineActionBar: ActionBar; checkAll: HTMLInputElement; inputContainer: HTMLElement; filterContainer: HTMLElement; @@ -129,7 +143,7 @@ export type Visibilities = { progressBar?: boolean; }; -class QuickInput extends Disposable implements IQuickInput { +abstract class QuickInput extends Disposable implements IQuickInput { protected static readonly noPromptMessage = localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel"); private _title: string | undefined; @@ -143,7 +157,9 @@ class QuickInput extends Disposable implements IQuickInput { private _contextKey: string | undefined; private _busy = false; private _ignoreFocusOut = false; - private _buttons: IQuickInputButton[] = []; + private _leftButtons: IQuickInputButton[] = []; + private _rightButtons: IQuickInputButton[] = []; + private _inlineButtons: IQuickInputButton[] = []; private buttonsUpdated = false; private _toggles: IQuickInputToggle[] = []; private togglesUpdated = false; @@ -161,6 +177,8 @@ class QuickInput extends Disposable implements IQuickInput { private busyDelay: TimeoutTimer | undefined; + abstract type: QuickInputType; + constructor( protected ui: QuickInputUI ) { @@ -190,7 +208,7 @@ class QuickInput extends Disposable implements IQuickInput { } set widget(widget: unknown | undefined) { - if (!(widget instanceof HTMLElement)) { + if (!(dom.isHTMLElement(widget))) { return; } if (this._widget !== widget) { @@ -257,12 +275,24 @@ class QuickInput extends Disposable implements IQuickInput { } } + protected get titleButtons() { + return this._leftButtons.length + ? [...this._leftButtons, this._rightButtons] + : this._rightButtons; + } + get buttons() { - return this._buttons; + return [ + ...this._leftButtons, + ...this._rightButtons, + ...this._inlineButtons + ]; } set buttons(buttons: IQuickInputButton[]) { - this._buttons = buttons; + this._leftButtons = buttons.filter(b => b === backButton); + this._rightButtons = buttons.filter(b => b !== backButton && b.location !== QuickInputButtonLocation.Inline); + this._inlineButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Inline); this.buttonsUpdated = true; this.update(); } @@ -391,8 +421,7 @@ class QuickInput extends Disposable implements IQuickInput { if (this.buttonsUpdated) { this.buttonsUpdated = false; this.ui.leftActionBar.clear(); - const leftButtons = this.buttons - .filter(button => button === backButton) + const leftButtons = this._leftButtons .map((button, index) => quickInputButtonToAction( button, `id-${index}`, @@ -400,14 +429,21 @@ class QuickInput extends Disposable implements IQuickInput { )); this.ui.leftActionBar.push(leftButtons, { icon: true, label: false }); this.ui.rightActionBar.clear(); - const rightButtons = this.buttons - .filter(button => button !== backButton) + const rightButtons = this._rightButtons .map((button, index) => quickInputButtonToAction( button, `id-${index}`, async () => this.onDidTriggerButtonEmitter.fire(button) )); this.ui.rightActionBar.push(rightButtons, { icon: true, label: false }); + this.ui.inlineActionBar.clear(); + const inlineButtons = this._inlineButtons + .map((button, index) => quickInputButtonToAction( + button, + `id-${index}`, + async () => this.onDidTriggerButtonEmitter.fire(button) + )); + this.ui.inlineActionBar.push(inlineButtons, { icon: true, label: false }); } if (this.togglesUpdated) { this.togglesUpdated = false; @@ -491,7 +527,7 @@ class QuickInput extends Disposable implements IQuickInput { } } -export class QuickPick extends QuickInput implements IQuickPick { +export class QuickPick extends QuickInput implements IQuickPick { private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); @@ -502,7 +538,7 @@ export class QuickPick extends QuickInput implements I private readonly onWillAcceptEmitter = this._register(new Emitter()); private readonly onDidAcceptEmitter = this._register(new Emitter()); private readonly onDidCustomEmitter = this._register(new Emitter()); - private _items: Array = []; + private _items: O extends { useSeparators: true } ? Array : Array = []; private itemsUpdated = false; private _canSelectMany = false; private _canAcceptInBackground = false; @@ -533,6 +569,9 @@ export class QuickPick extends QuickInput implements I private _hideInput: boolean | undefined; private _hideCountBadge: boolean | undefined; private _hideCheckAll: boolean | undefined; + private _focusEventBufferer = new EventBufferer(); + + readonly type = QuickInputType.QuickPick; get quickNavigate() { return this._quickNavigate; @@ -606,7 +645,7 @@ export class QuickPick extends QuickInput implements I this.ui.list.scrollTop = scrollTop; } - set items(items: Array) { + set items(items: O extends { useSeparators: true } ? Array : Array) { this._items = items; this.itemsUpdated = true; this.update(); @@ -816,7 +855,7 @@ export class QuickPick extends QuickInput implements I private trySelectFirst() { if (!this.canSelectMany) { - this.ui.list.focus(QuickInputListFocus.First); + this.ui.list.focus(QuickPickFocus.First); } } @@ -826,75 +865,6 @@ export class QuickPick extends QuickInput implements I this.ui.inputBox.onDidChange(value => { this.doSetValue(value, true /* skip update since this originates from the UI */); })); - // Keybindings for the input box or list if there is no input box - this.visibleDisposables.add((this._hideInput ? this.ui.list : this.ui.inputBox).onKeyDown((event: KeyboardEvent | StandardKeyboardEvent) => { - switch (event.keyCode) { - case KeyCode.DownArrow: - if (isMacintosh ? event.metaKey : event.altKey) { - this.ui.list.focus(QuickInputListFocus.NextSeparator); - } else { - this.ui.list.focus(QuickInputListFocus.Next); - } - if (this.canSelectMany) { - this.ui.list.domFocus(); - } - dom.EventHelper.stop(event, true); - break; - case KeyCode.UpArrow: - if (isMacintosh ? event.metaKey : event.altKey) { - this.ui.list.focus(QuickInputListFocus.PreviousSeparator); - } else { - this.ui.list.focus(QuickInputListFocus.Previous); - } - if (this.canSelectMany) { - this.ui.list.domFocus(); - } - dom.EventHelper.stop(event, true); - break; - case KeyCode.PageDown: - this.ui.list.focus(QuickInputListFocus.NextPage); - if (this.canSelectMany) { - this.ui.list.domFocus(); - } - dom.EventHelper.stop(event, true); - break; - case KeyCode.PageUp: - this.ui.list.focus(QuickInputListFocus.PreviousPage); - if (this.canSelectMany) { - this.ui.list.domFocus(); - } - dom.EventHelper.stop(event, true); - break; - case KeyCode.RightArrow: - if (!this._canAcceptInBackground) { - return; // needs to be enabled - } - - if (!this.ui.inputBox.isSelectionAtEnd()) { - return; // ensure input box selection at end - } - - if (this.activeItems[0]) { - this._selectedItems = [this.activeItems[0]]; - this.onDidChangeSelectionEmitter.fire(this.selectedItems); - this.handleAccept(true); - } - - break; - case KeyCode.Home: - if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) { - this.ui.list.focus(QuickInputListFocus.First); - dom.EventHelper.stop(event, true); - } - break; - case KeyCode.End: - if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) { - this.ui.list.focus(QuickInputListFocus.Last); - dom.EventHelper.stop(event, true); - } - break; - } - })); this.visibleDisposables.add(this.ui.onDidAccept(() => { if (this.canSelectMany) { // if there are no checked elements, it means that an onDidChangeSelection never fired to overwrite @@ -914,7 +884,11 @@ export class QuickPick extends QuickInput implements I this.visibleDisposables.add(this.ui.onDidCustom(() => { this.onDidCustomEmitter.fire(); })); - this.visibleDisposables.add(this.ui.list.onDidChangeFocus(focusedItems => { + this.visibleDisposables.add(this._focusEventBufferer.wrapEvent( + this.ui.list.onDidChangeFocus, + // Only fire the last event + (_, e) => e + )(focusedItems => { if (this.activeItemsUpdated) { return; // Expect another event. } @@ -941,7 +915,7 @@ export class QuickPick extends QuickInput implements I } })); this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => { - if (!this.canSelectMany) { + if (!this.canSelectMany || !this.visible) { return; } if (this.selectedItemsToConfirm !== this._selectedItems && equals(checkedItems, this._selectedItems, (a, b) => a === b)) { @@ -1032,7 +1006,7 @@ export class QuickPick extends QuickInput implements I const scrollTopBefore = this.keepScrollPosition ? this.scrollTop : 0; const hasDescription = !!this.description; const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: hasDescription, checkAll: this.canSelectMany && !this._hideCheckAll, checkBox: this.canSelectMany, @@ -1077,37 +1051,28 @@ export class QuickPick extends QuickInput implements I this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; - const currentActiveItems = this._activeItems; - this.ui.list.setElements(this.items); - this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); - this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); - this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); - this.ui.count.setCount(this.ui.list.getCheckedCount()); - switch (this._itemActivation) { - case ItemActivation.NONE: - // Handle the case where we had active items (i.e. someone chose an item) - // but the initial item activation is set to none. Calling clearFocus will - // not trigger the onDidFocus event because when the tree receives new elements, - // it sets the focus to no elements. So we need to set & fire the active items - // accordingly to reflect the state change after setting the items. - if (currentActiveItems.length > 0) { - this._activeItems = []; - this.onDidChangeActiveEmitter.fire(this._activeItems); - } - this._itemActivation = ItemActivation.FIRST; // only valid once, then unset - break; - case ItemActivation.SECOND: - this.ui.list.focus(QuickInputListFocus.Second); - this._itemActivation = ItemActivation.FIRST; // only valid once, then unset - break; - case ItemActivation.LAST: - this.ui.list.focus(QuickInputListFocus.Last); - this._itemActivation = ItemActivation.FIRST; // only valid once, then unset - break; - default: - this.trySelectFirst(); - break; - } + this._focusEventBufferer.bufferEvents(() => { + this.ui.list.setElements(this.items); + // We want focus to exist in the list if there are items so that space can be used to toggle + this.ui.list.shouldLoop = !this.canSelectMany; + this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); + switch (this._itemActivation) { + case ItemActivation.NONE: + this._itemActivation = ItemActivation.FIRST; // only valid once, then unset + break; + case ItemActivation.SECOND: + this.ui.list.focus(QuickPickFocus.Second); + this._itemActivation = ItemActivation.FIRST; // only valid once, then unset + break; + case ItemActivation.LAST: + this.ui.list.focus(QuickPickFocus.Last); + this._itemActivation = ItemActivation.FIRST; // only valid once, then unset + break; + default: + this.trySelectFirst(); + break; + } + }); } if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { if (this.canSelectMany) { @@ -1145,7 +1110,7 @@ export class QuickPick extends QuickInput implements I // Focus the first element in the list if multiselect is enabled if (this.canSelectMany) { - this.ui.list.focus(QuickInputListFocus.First); + this.ui.list.focus(QuickPickFocus.First); } } @@ -1154,6 +1119,26 @@ export class QuickPick extends QuickInput implements I this.scrollTop = scrollTopBefore; } } + + focus(focus: QuickPickFocus): void { + this.ui.list.focus(focus); + // To allow things like space to check/uncheck items + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + } + + accept(inBackground?: boolean | undefined): void { + if (inBackground && !this._canAcceptInBackground) { + return; // needs to be enabled + } + + if (this.activeItems[0]) { + this._selectedItems = [this.activeItems[0]]; + this.onDidChangeSelectionEmitter.fire(this.selectedItems); + this.handleAccept(inBackground ?? false); + } + } } export class InputBox extends QuickInput implements IInputBox { @@ -1166,6 +1151,8 @@ export class InputBox extends QuickInput implements IInputBox { private readonly onDidValueChangeEmitter = this._register(new Emitter()); private readonly onDidAcceptEmitter = this._register(new Emitter()); + readonly type = QuickInputType.InputBox; + get value() { return this._value; } @@ -1246,7 +1233,7 @@ export class InputBox extends QuickInput implements IInputBox { this.ui.container.classList.remove('hidden-input'); const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: !!this.description || !!this.step, inputBox: true, message: true, @@ -1272,13 +1259,15 @@ export class InputBox extends QuickInput implements IInputBox { } export class QuickWidget extends QuickInput implements IQuickWidget { + readonly type = QuickInputType.QuickWidget; + protected override update() { if (!this.visible) { return; } const visibilities: Visibilities = { - title: !!this.title || !!this.step || !!this.buttons.length, + title: !!this.title || !!this.step || !!this.titleButtons.length, description: !!this.description || !!this.step }; @@ -1299,7 +1288,7 @@ export class QuickInputHoverDelegate extends WorkbenchHoverDelegate { private getOverrideOptions(options: IHoverDelegateOptions): Partial { // Only show the hover hint if the content is of a decent size const showHoverHint = ( - options.content instanceof HTMLElement + dom.isHTMLElement(options.content) ? options.content.textContent ?? '' : typeof options.content === 'string' ? options.content diff --git a/src/vs/platform/quickinput/browser/quickInputActions.ts b/src/vs/platform/quickinput/browser/quickInputActions.ts new file mode 100644 index 00000000000..5efaf793645 --- /dev/null +++ b/src/vs/platform/quickinput/browser/quickInputActions.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { isMacintosh } from 'vs/base/common/platform'; +import { PartialExcept } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { ICommandHandler } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ICommandAndKeybindingRule, KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { endOfQuickInputBoxContext, inQuickInputContext, quickInputTypeContextKeyValue } from 'vs/platform/quickinput/browser/quickInput'; +import { IQuickInputService, IQuickPick, QuickInputType, QuickPickFocus } from 'vs/platform/quickinput/common/quickInput'; + +const defaultCommandAndKeybindingRule = { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(ContextKeyExpr.equals(quickInputTypeContextKeyValue, QuickInputType.QuickPick), inQuickInputContext), + metadata: { description: localize('quickPick', "Used while in the context of the quick pick. If you change one keybinding for this command, you should change all of the other keybindings (modifier variants) of this command as well.") } +}; +function registerQuickPickCommandAndKeybindingRule(rule: PartialExcept, options: { withAltMod?: boolean; withCtrlMod?: boolean; withCmdMod?: boolean } = {}) { + KeybindingsRegistry.registerCommandAndKeybindingRule({ + ...defaultCommandAndKeybindingRule, + ...rule, + secondary: getSecondary(rule.primary!, rule.secondary ?? [], options) + }); +} + +const ctrlKeyMod = isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd; + +// This function will generate all the combinations of keybindings for the given primary keybinding +function getSecondary(primary: number, secondary: number[], options: { withAltMod?: boolean; withCtrlMod?: boolean; withCmdMod?: boolean } = {}): number[] { + if (options.withAltMod) { + secondary.push(KeyMod.Alt + primary); + } + if (options.withCtrlMod) { + secondary.push(ctrlKeyMod + primary); + if (options.withAltMod) { + secondary.push(KeyMod.Alt + ctrlKeyMod + primary); + } + } + + if (options.withCmdMod && isMacintosh) { + secondary.push(KeyMod.CtrlCmd + primary); + if (options.withCtrlMod) { + secondary.push(KeyMod.CtrlCmd + KeyMod.WinCtrl + primary); + } + if (options.withAltMod) { + secondary.push(KeyMod.CtrlCmd + KeyMod.Alt + primary); + if (options.withCtrlMod) { + secondary.push(KeyMod.CtrlCmd + KeyMod.Alt + KeyMod.WinCtrl + primary); + } + } + } + + return secondary; +} + +//#region Navigation + +function focusHandler(focus: QuickPickFocus, focusOnQuickNatigate?: QuickPickFocus): ICommandHandler { + return accessor => { + // Assuming this is a quick pick due to above when clause + const currentQuickPick = accessor.get(IQuickInputService).currentQuickInput as IQuickPick | undefined; + if (!currentQuickPick) { + return; + } + if (focusOnQuickNatigate && currentQuickPick.quickNavigate) { + return currentQuickPick.focus(focusOnQuickNatigate); + } + return currentQuickPick.focus(focus); + }; +} + +registerQuickPickCommandAndKeybindingRule( + { id: 'quickInput.pageNext', primary: KeyCode.PageDown, handler: focusHandler(QuickPickFocus.NextPage) }, + { withAltMod: true, withCtrlMod: true, withCmdMod: true } +); +registerQuickPickCommandAndKeybindingRule( + { id: 'quickInput.pagePrevious', primary: KeyCode.PageUp, handler: focusHandler(QuickPickFocus.PreviousPage) }, + { withAltMod: true, withCtrlMod: true, withCmdMod: true } +); +registerQuickPickCommandAndKeybindingRule( + { id: 'quickInput.first', primary: ctrlKeyMod + KeyCode.Home, handler: focusHandler(QuickPickFocus.First) }, + { withAltMod: true, withCmdMod: true } +); +registerQuickPickCommandAndKeybindingRule( + { id: 'quickInput.last', primary: ctrlKeyMod + KeyCode.End, handler: focusHandler(QuickPickFocus.Last) }, + { withAltMod: true, withCmdMod: true } +); +registerQuickPickCommandAndKeybindingRule( + { id: 'quickInput.next', primary: KeyCode.DownArrow, handler: focusHandler(QuickPickFocus.Next) }, + { withCtrlMod: true } +); +registerQuickPickCommandAndKeybindingRule( + { id: 'quickInput.previous', primary: KeyCode.UpArrow, handler: focusHandler(QuickPickFocus.Previous) }, + { withCtrlMod: true } +); + +// The next & previous separator commands are interesting because if we are in quick access mode, we are already holding a modifier key down. +// In this case, we want that modifier key+up/down to navigate to the next/previous item, not the next/previous separator. +// To handle this, we have a separate command for navigating to the next/previous separator when we are not in quick access mode. +// If, however, we are in quick access mode, and you hold down an additional modifier key, we will navigate to the next/previous separator. + +const nextSeparatorFallbackDesc = localize('quickInput.nextSeparatorWithQuickAccessFallback', "If we're in quick access mode, this will navigate to the next item. If we are not in quick access mode, this will navigate to the next separator."); +const prevSeparatorFallbackDesc = localize('quickInput.previousSeparatorWithQuickAccessFallback', "If we're in quick access mode, this will navigate to the previous item. If we are not in quick access mode, this will navigate to the previous separator."); +if (isMacintosh) { + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.nextSeparatorWithQuickAccessFallback', + primary: KeyMod.CtrlCmd + KeyCode.DownArrow, + handler: focusHandler(QuickPickFocus.NextSeparator, QuickPickFocus.Next), + metadata: { description: nextSeparatorFallbackDesc } + }, + ); + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.nextSeparator', + primary: KeyMod.CtrlCmd + KeyMod.Alt + KeyCode.DownArrow, + // Since macOS has the cmd key as the primary modifier, we need to add this additional + // keybinding to capture cmd+ctrl+upArrow + secondary: [KeyMod.CtrlCmd + KeyMod.WinCtrl + KeyCode.DownArrow], + handler: focusHandler(QuickPickFocus.NextSeparator) + }, + { withCtrlMod: true } + ); + + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.previousSeparatorWithQuickAccessFallback', + primary: KeyMod.CtrlCmd + KeyCode.UpArrow, + handler: focusHandler(QuickPickFocus.PreviousSeparator, QuickPickFocus.Previous), + metadata: { description: prevSeparatorFallbackDesc } + }, + ); + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.previousSeparator', + primary: KeyMod.CtrlCmd + KeyMod.Alt + KeyCode.UpArrow, + // Since macOS has the cmd key as the primary modifier, we need to add this additional + // keybinding to capture cmd+ctrl+upArrow + secondary: [KeyMod.CtrlCmd + KeyMod.WinCtrl + KeyCode.UpArrow], + handler: focusHandler(QuickPickFocus.PreviousSeparator) + }, + { withCtrlMod: true } + ); +} else { + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.nextSeparatorWithQuickAccessFallback', + primary: KeyMod.Alt + KeyCode.DownArrow, + handler: focusHandler(QuickPickFocus.NextSeparator, QuickPickFocus.Next), + metadata: { description: nextSeparatorFallbackDesc } + }, + ); + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.nextSeparator', + primary: KeyMod.CtrlCmd + KeyMod.Alt + KeyCode.DownArrow, + handler: focusHandler(QuickPickFocus.NextSeparator) + }, + ); + + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.previousSeparatorWithQuickAccessFallback', + primary: KeyMod.Alt + KeyCode.UpArrow, + handler: focusHandler(QuickPickFocus.PreviousSeparator, QuickPickFocus.Previous), + metadata: { description: prevSeparatorFallbackDesc } + }, + ); + registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.previousSeparator', + primary: KeyMod.CtrlCmd + KeyMod.Alt + KeyCode.UpArrow, + handler: focusHandler(QuickPickFocus.PreviousSeparator) + }, + ); +} + +//#endregion + +//#region Accept + +registerQuickPickCommandAndKeybindingRule( + { + id: 'quickInput.acceptInBackground', + // If we are in the quick pick but the input box is not focused or our cursor is at the end of the input box + when: ContextKeyExpr.and(defaultCommandAndKeybindingRule.when, ContextKeyExpr.or(InputFocusedContext.negate(), endOfQuickInputBoxContext)), + primary: KeyCode.RightArrow, + // Need a little extra weight to ensure this keybinding is preferred over the default cmd+alt+right arrow keybinding + // https://github.com/microsoft/vscode/blob/1451e4fbbbf074a4355cc537c35b547b80ce1c52/src/vs/workbench/browser/parts/editor/editorActions.ts#L1178-L1195 + weight: KeybindingWeight.WorkbenchContrib + 50, + handler: (accessor) => { + const currentQuickPick = accessor.get(IQuickInputService).currentQuickInput as IQuickPick; + currentQuickPick?.accept(true); + }, + }, + { withAltMod: true, withCtrlMod: true, withCmdMod: true } +); diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index e64440ba2a7..8dc3875e13f 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -16,13 +16,15 @@ import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { isString } from 'vs/base/common/types'; import { localize } from 'vs/nls'; -import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput, QuickPickFocus } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from 'vs/platform/quickinput/browser/quickInputBox'; -import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget } from 'vs/platform/quickinput/browser/quickInput'; +import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget, InQuickInputContextKey, QuickInputTypeContextKey, EndOfQuickInputBoxContextKey } from 'vs/platform/quickinput/browser/quickInput'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { mainWindow } from 'vs/base/browser/window'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { QuickInputListFocus, QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; +import { QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import 'vs/platform/quickinput/browser/quickInputActions'; const $ = dom.$; @@ -40,6 +42,7 @@ export class QuickInputController extends Disposable { private keyMods: Writeable = { ctrlCmd: false, alt: false }; private controller: IQuickInput | null = null; + get currentQuickInput() { return this.controller ?? undefined; } private _container: HTMLElement; get container() { return this._container; } @@ -54,10 +57,15 @@ export class QuickInputController extends Disposable { private previousFocusElement?: HTMLElement; + private readonly inQuickInputContext = InQuickInputContextKey.bindTo(this.contextKeyService); + private readonly quickInputTypeContext = QuickInputTypeContextKey.bindTo(this.contextKeyService); + private readonly endOfQuickInputBoxContext = EndOfQuickInputBoxContextKey.bindTo(this.contextKeyService); + constructor( private options: IQuickInputOptions, @ILayoutService private readonly layoutService: ILayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); this.idPrefix = options.idPrefix; @@ -148,6 +156,9 @@ export class QuickInputController extends Disposable { countContainer.setAttribute('aria-live', 'polite'); const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }, this.styles.countBadge); + const inlineActionBar = this._register(new ActionBar(headerContainer, { hoverDelegate: this.options.hoverDelegate })); + inlineActionBar.domNode.classList.add('quick-input-inline-action-bar'); + const okContainer = dom.append(headerContainer, $('.quick-input-action')); const ok = this._register(new Button(okContainer, this.styles.button)); ok.label = localize('ok', "OK"); @@ -204,18 +215,34 @@ export class QuickInputController extends Disposable { const focusTracker = dom.trackFocus(container); this._register(focusTracker); this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, e => { + const ui = this.getUI(); + if (dom.isAncestor(e.relatedTarget as HTMLElement, ui.inputContainer)) { + const value = ui.inputBox.isSelectionAtEnd(); + if (this.endOfQuickInputBoxContext.get() !== value) { + this.endOfQuickInputBoxContext.set(value); + } + } // Ignore focus events within container - if (dom.isAncestor(e.relatedTarget as HTMLElement, container)) { + if (dom.isAncestor(e.relatedTarget as HTMLElement, ui.container)) { return; } - this.previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined; + this.inQuickInputContext.set(true); + this.previousFocusElement = dom.isHTMLElement(e.relatedTarget) ? e.relatedTarget : undefined; }, true)); this._register(focusTracker.onDidBlur(() => { if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) { this.hide(QuickInputHideReason.Blur); } + this.inQuickInputContext.set(false); + this.endOfQuickInputBoxContext.set(false); this.previousFocusElement = undefined; })); + this._register(inputBox.onKeyDown(_ => { + const value = this.getUI().inputBox.isSelectionAtEnd(); + if (this.endOfQuickInputBoxContext.get() !== value) { + this.endOfQuickInputBoxContext.set(value); + } + })); this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, (e: FocusEvent) => { inputBox.setFocus(); })); @@ -250,7 +277,7 @@ export class QuickInputController extends Disposable { } else { selectors.push('input[type=text]'); } - if (this.getUI().list.isDisplayed()) { + if (this.getUI().list.displayed) { selectors.push('.monaco-list'); } // focus links if there are any @@ -296,6 +323,7 @@ export class QuickInputController extends Disposable { description2, widget, rightActionBar, + inlineActionBar, checkAll, inputContainer, filterContainer, @@ -334,7 +362,7 @@ export class QuickInputController extends Disposable { } } - pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { + pick>(picks: Promise[]> | QuickPickInput[], options: IPickOptions = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { type R = (O extends { canPickMany: true } ? T[] : T) | undefined; return new Promise((doResolve, reject) => { let resolve = (result: R) => { @@ -346,7 +374,7 @@ export class QuickInputController extends Disposable { resolve(undefined); return; } - const input = this.createQuickPick(); + const input = this.createQuickPick({ useSeparators: true }); let activeItem: T | undefined; const disposables = [ input, @@ -517,9 +545,11 @@ export class QuickInputController extends Disposable { backButton = backButton; - createQuickPick(): IQuickPick { + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; + createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { const ui = this.getUI(true); - return new QuickPick(ui); + return new QuickPick(ui); } createInputBox(): IInputBox { @@ -546,6 +576,7 @@ export class QuickInputController extends Disposable { ui.description2.textContent = ''; dom.reset(ui.widget); ui.rightActionBar.clear(); + ui.inlineActionBar.clear(); ui.checkAll.checked = false; // ui.inputBox.value = ''; Avoid triggering an event. ui.inputBox.placeholder = ''; @@ -569,6 +600,7 @@ export class QuickInputController extends Disposable { ui.container.style.display = ''; this.updateLayout(); ui.inputBox.setFocus(); + this.quickInputTypeContext.set(controller.type); } isVisible(): boolean { @@ -589,7 +621,7 @@ export class QuickInputController extends Disposable { ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; ui.message.style.display = visibilities.message ? '' : 'none'; ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none'; - ui.list.display(!!visibilities.list); + ui.list.displayed = !!visibilities.list; ui.container.classList.toggle('show-checkboxes', !!visibilities.checkBox); ui.container.classList.toggle('hidden-input', !visibilities.inputBox && !visibilities.description); this.updateLayout(); // TODO @@ -658,8 +690,8 @@ export class QuickInputController extends Disposable { } navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { - if (this.isVisible() && this.getUI().list.isDisplayed()) { - this.getUI().list.focus(next ? QuickInputListFocus.Next : QuickInputListFocus.Previous); + if (this.isVisible() && this.getUI().list.displayed) { + this.getUI().list.focus(next ? QuickPickFocus.Next : QuickPickFocus.Previous); if (quickNavigate && this.controller instanceof QuickPick) { this.controller.quickNavigate = quickNavigate; } diff --git a/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index e23c1158e68..3c5b30a95e6 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/src/vs/platform/quickinput/browser/quickInputService.ts @@ -42,6 +42,7 @@ export class QuickInputService extends Themable implements IQuickInputService { } private get hasController() { return !!this._controller; } + get currentQuickInput() { return this.controller.currentQuickInput; } private _quickAccess: IQuickAccessController | undefined; get quickAccess(): IQuickAccessController { @@ -148,7 +149,7 @@ export class QuickInputService extends Themable implements IQuickInputService { }); } - pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { + pick>(picks: Promise[]> | QuickPickInput[], options?: O, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> { return this.controller.pick(picks, options, token); } @@ -156,8 +157,10 @@ export class QuickInputService extends Themable implements IQuickInputService { return this.controller.input(options, token); } - createQuickPick(): IQuickPick { - return this.controller.createQuickPick(); + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; + createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { + return this.controller.createQuickPick(options); } createInputBox(): IInputBox { diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 672f581e755..74163bd7e36 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -4,30 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event, EventBufferer, IValueWithChangeEvent } from 'vs/base/common/event'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IObjectTreeElement, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem, QuickPickFocus } from 'vs/platform/quickinput/common/quickInput'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IMatch } from 'vs/base/common/filters'; import { IListAccessibilityProvider, IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { OS, isMacintosh } from 'vs/base/common/platform'; +import { OS } from 'vs/base/common/platform'; import { memoize } from 'vs/base/common/decorators'; import { IIconLabelValueOptions, IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { isDark } from 'vs/platform/theme/common/theme'; import { URI } from 'vs/base/common/uri'; -import { IHoverWidget, ITooltipMarkdownString } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { quickInputButtonToAction } from 'vs/platform/quickinput/browser/quickInputUtils'; import { Lazy } from 'vs/base/common/lazy'; import { IParsedLabelWithIcons, getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; @@ -37,21 +36,13 @@ import { ltrim } from 'vs/base/common/strings'; import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; import { ThrottledDelayer } from 'vs/base/common/async'; import { isCancellationError } from 'vs/base/common/errors'; +import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { observableValue, observableValueOpts, transaction } from 'vs/base/common/observable'; +import { equals } from 'vs/base/common/arrays'; const $ = dom.$; -export enum QuickInputListFocus { - First = 1, - Second, - Last, - Next, - Previous, - NextPage, - PreviousPage, - NextSeparator, - PreviousSeparator -} - interface IQuickInputItemLazyParts { readonly saneLabel: string; readonly saneSortLabel: string; @@ -312,14 +303,14 @@ class QuickInputAccessibilityProvider implements IListAccessibilityProvider | undefined { if (!element.hasCheckbox || !(element instanceof QuickPickItemElement)) { return undefined; } return { - value: element.checked, - onDidChange: element.onChecked + get value() { return element.checked; }, + onDidChange: e => element.onChecked(() => e()), }; } } @@ -444,7 +435,7 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer, index: number, data: IQuickInputItemTemplateData): void { const element = node.element; data.element = element; @@ -576,7 +573,7 @@ class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer(); /** * Event that is fired when the tree receives a keydown. @@ -678,17 +677,17 @@ export class QuickInputTree extends Disposable { */ readonly onLeave: Event = this._onLeave.event; - private readonly _onChangedAllVisibleChecked = new Emitter(); - onChangedAllVisibleChecked: Event = this._onChangedAllVisibleChecked.event; + private readonly _visibleCountObservable = observableValue('VisibleCount', 0); + onChangedVisibleCount: Event = Event.fromObservable(this._visibleCountObservable, this._store); - private readonly _onChangedCheckedCount = new Emitter(); - onChangedCheckedCount: Event = this._onChangedCheckedCount.event; + private readonly _allVisibleCheckedObservable = observableValue('AllVisibleChecked', false); + onChangedAllVisibleChecked: Event = Event.fromObservable(this._allVisibleCheckedObservable, this._store); - private readonly _onChangedVisibleCount = new Emitter(); - onChangedVisibleCount: Event = this._onChangedVisibleCount.event; + private readonly _checkedCountObservable = observableValue('CheckedCount', 0); + onChangedCheckedCount: Event = Event.fromObservable(this._checkedCountObservable, this._store); - private readonly _onChangedCheckedElements = new Emitter(); - onChangedCheckedElements: Event = this._onChangedCheckedElements.event; + private readonly _checkedElementsObservable = observableValueOpts({ equalsFn: equals }, new Array()); + onChangedCheckedElements: Event = Event.fromObservable(this._checkedElementsObservable, this._store); private readonly _onButtonTriggered = new Emitter>(); onButtonTriggered = this._onButtonTriggered.event; @@ -696,28 +695,32 @@ export class QuickInputTree extends Disposable { private readonly _onSeparatorButtonTriggered = new Emitter(); onSeparatorButtonTriggered = this._onSeparatorButtonTriggered.event; + private readonly _elementChecked = new Emitter<{ element: IQuickPickElement; checked: boolean }>(); + private readonly _elementCheckedEventBufferer = new EventBufferer(); + + //#endregion + + private _hasCheckboxes = false; + private readonly _container: HTMLElement; private readonly _tree: WorkbenchObjectTree; private readonly _separatorRenderer: QuickPickSeparatorElementRenderer; private readonly _itemRenderer: QuickPickItemElementRenderer; - private readonly _elementChecked = new Emitter<{ element: IQuickPickElement; checked: boolean }>(); private _inputElements = new Array(); private _elementTree = new Array(); private _itemElements = new Array(); // Elements that apply to the current set of elements - private _elementDisposable = this._register(new DisposableStore()); + private readonly _elementDisposable = this._register(new DisposableStore()); private _lastHover: IHoverWidget | undefined; - // This is used to prevent setting the checked state of a single element from firing the checked events - // so that we can batch them together. This can probably be improved by handling events differently, - // but this works for now. An observable would probably be ideal for this. - private _shouldFireCheckedEvents = true; + private _lastQueryString: string | undefined; constructor( private parent: HTMLElement, private hoverDelegate: IHoverDelegate, private linkOpenerDelegate: (content: string) => void, id: string, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(); this._container = dom.append(this.parent, $('.quick-input-list')); @@ -730,6 +733,24 @@ export class QuickInputTree extends Disposable { new QuickInputItemDelegate(), [this._itemRenderer, this._separatorRenderer], { + filter: { + filter(element) { + return element.hidden + ? TreeVisibility.Hidden + : element instanceof QuickPickSeparatorElement + ? TreeVisibility.Recurse + : TreeVisibility.Visible; + }, + }, + sorter: { + compare: (element, otherElement) => { + if (!this.sortByLabel || !this._lastQueryString) { + return 0; + } + const normalizedSearchValue = this._lastQueryString.toLowerCase(); + return compareEntries(element, otherElement, normalizedSearchValue); + }, + }, accessibilityProvider: new QuickInputAccessibilityProvider(), setRowLineHeight: false, multipleSelectionSupport: false, @@ -739,17 +760,6 @@ export class QuickInputTree extends Disposable { indent: 0, horizontalScrolling: false, allowNonCollapsibleParents: true, - identityProvider: { - getId: element => { - // always prefer item over separator because if item is defined, it must be the main item type - // always prefer a defined id if one was specified and use label as a fallback - return element.item?.id - ?? element.item?.label - ?? element.separator?.id - ?? element.separator?.label - ?? ''; - } - }, alwaysConsumeMouseWheel: true } )); @@ -763,7 +773,8 @@ export class QuickInputTree extends Disposable { get onDidChangeFocus() { return Event.map( this._tree.onDidChangeFocus, - e => e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item) + e => e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item), + this._store ); } @@ -774,7 +785,17 @@ export class QuickInputTree extends Disposable { e => ({ items: e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item), event: e.browserEvent - })); + }), + this._store + ); + } + + get displayed() { + return this._container.style.display !== 'none'; + } + + set displayed(value: boolean) { + this._container.style.display = value ? '' : 'none'; } get scrollTop() { @@ -845,6 +866,14 @@ export class QuickInputTree extends Disposable { this._sortByLabel = value; } + private _shouldLoop = true; + get shouldLoop() { + return this._shouldLoop; + } + set shouldLoop(value: boolean) { + this._shouldLoop = value; + } + //#endregion //#region register listeners @@ -853,6 +882,7 @@ export class QuickInputTree extends Disposable { this._registerOnKeyDown(); this._registerOnContainerClick(); this._registerOnMouseMiddleClick(); + this._registerOnTreeModelChanged(); this._registerOnElementChecked(); this._registerOnContextMenu(); this._registerHoverListeners(); @@ -868,27 +898,6 @@ export class QuickInputTree extends Disposable { case KeyCode.Space: this.toggleCheckbox(); break; - case KeyCode.KeyA: - if (isMacintosh ? e.metaKey : e.ctrlKey) { - this._tree.setFocus(this._itemElements); - } - break; - // When we hit the top of the tree, we fire the onLeave event. - case KeyCode.UpArrow: { - const focus1 = this._tree.getFocus(); - if (focus1.length === 1 && focus1[0] === this._itemElements[0]) { - this._onLeave.fire(); - } - break; - } - // When we hit the bottom of the tree, we fire the onLeave event. - case KeyCode.DownArrow: { - const focus2 = this._tree.getFocus(); - if (focus2.length === 1 && focus2[0] === this._itemElements[this._itemElements.length - 1]) { - this._onLeave.fire(); - } - break; - } } this._onKeyDown.fire(event); @@ -911,8 +920,19 @@ export class QuickInputTree extends Disposable { })); } + private _registerOnTreeModelChanged() { + this._register(this._tree.onDidChangeModel(() => { + const visibleCount = this._itemElements.filter(e => !e.hidden).length; + this._visibleCountObservable.set(visibleCount, undefined); + if (this._hasCheckboxes) { + this._updateCheckedObservables(); + } + })); + } + private _registerOnElementChecked() { - this._register(this._elementChecked.event(_ => this._fireCheckedEvents())); + // Only fire the last event when buffered + this._register(this._elementCheckedEventBufferer.wrapEvent(this._elementChecked.event, (_, e) => e)(_ => this._updateCheckedObservables())); } private _registerOnContextMenu() { @@ -935,13 +955,13 @@ export class QuickInputTree extends Disposable { this._register(this._tree.onMouseOver(async e => { // If we hover over an anchor element, we don't want to show the hover because // the anchor may have a tooltip that we want to show instead. - if (e.browserEvent.target instanceof HTMLAnchorElement) { + if (dom.isHTMLAnchorElement(e.browserEvent.target)) { delayer.cancel(); return; } if ( // anchors are an exception as called out above so we skip them here - !(e.browserEvent.relatedTarget instanceof HTMLAnchorElement) && + !(dom.isHTMLAnchorElement(e.browserEvent.relatedTarget)) && // check if the mouse is still over the same element dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node) ) { @@ -1047,37 +1067,22 @@ export class QuickInputTree extends Disposable { //#region public methods - getAllVisibleChecked() { - return this._allVisibleChecked(this._itemElements, false); - } - - getCheckedCount() { - return this._itemElements.filter(element => element.checked).length; - } - - getVisibleCount() { - return this._itemElements.filter(e => !e.hidden).length; - } - setAllVisibleChecked(checked: boolean) { - try { - this._shouldFireCheckedEvents = false; + this._elementCheckedEventBufferer.bufferEvents(() => { this._itemElements.forEach(element => { if (!element.hidden && !element.checkboxDisabled) { - // Would fire an event if we didn't have the flag set + // Would fire an event if we didn't beffer the events element.checked = checked; } }); - } finally { - this._shouldFireCheckedEvents = true; - this._fireCheckedEvents(); - } + }); } setElements(inputElements: QuickPickItem[]): void { this._elementDisposable.clear(); + this._lastQueryString = undefined; this._inputElements = inputElements; - const hasCheckbox = this.parent.classList.contains('show-checkboxes'); + this._hasCheckboxes = this.parent.classList.contains('show-checkboxes'); let currentSeparatorElement: QuickPickSeparatorElement | undefined; this._itemElements = new Array(); this._elementTree = inputElements.reduce((result, item, index) => { @@ -1089,7 +1094,7 @@ export class QuickInputTree extends Disposable { } currentSeparatorElement = new QuickPickSeparatorElement( index, - (event: IQuickPickSeparatorButtonEvent) => this.fireSeparatorButtonTriggered(event), + e => this._onSeparatorButtonTriggered.fire(e), item ); element = currentSeparatorElement; @@ -1103,8 +1108,8 @@ export class QuickInputTree extends Disposable { } const qpi = new QuickPickItemElement( index, - hasCheckbox, - (event: IQuickPickItemButtonEvent) => this.fireButtonTriggered(event), + this._hasCheckboxes, + e => this._onButtonTriggered.fire(e), this._elementChecked, item, separator, @@ -1122,48 +1127,27 @@ export class QuickInputTree extends Disposable { return result; }, new Array()); - const elements = new Array>(); - let visibleCount = 0; - for (const element of this._elementTree) { - if (element instanceof QuickPickSeparatorElement) { - elements.push({ - element, - collapsible: false, - collapsed: false, - children: element.children.map(e => ({ - element: e, - collapsible: false, - collapsed: false, - })), - }); - visibleCount += element.children.length + 1; // +1 for the separator itself; - } else { - elements.push({ - element, - collapsible: false, - collapsed: false, - }); - visibleCount++; - } + this._setElementsToTree(this._elementTree); + + // Accessibility hack, unfortunately on next tick + // https://github.com/microsoft/vscode/issues/211976 + if (this.accessibilityService.isScreenReaderOptimized()) { + setTimeout(() => { + const focusedElement = this._tree.getHTMLElement().querySelector(`.monaco-list-row.focused`); + const parent = focusedElement?.parentNode; + if (focusedElement && parent) { + const nextSibling = focusedElement.nextSibling; + focusedElement.remove(); + parent.insertBefore(focusedElement, nextSibling); + } + }, 0); } - this._tree.setChildren(null, elements); - this._onChangedVisibleCount.fire(visibleCount); - } - - getElementsCount(): number { - return this._inputElements.length; - } - - getFocusedElements() { - return this._tree.getFocus() - .filter((e): e is IQuickPickElement => !!e) - .map(e => e.item) - .filter((e): e is IQuickPickItem => !!e); } setFocusedElements(items: IQuickPickItem[]) { const elements = items.map(item => this._itemElements.find(e => e.item === item)) - .filter((e): e is QuickPickItemElement => !!e); + .filter((e): e is QuickPickItemElement => !!e) + .filter(e => !e.hidden); this._tree.setFocus(elements); if (items.length > 0) { const focused = this._tree.getFocus()[0]; @@ -1177,12 +1161,6 @@ export class QuickInputTree extends Disposable { return this._tree.getHTMLElement().getAttribute('aria-activedescendant'); } - getSelectedElements() { - return this._tree.getSelection() - .filter((e): e is IQuickPickElement => !!e && !!(e as QuickPickItemElement).item) - .map(e => e.item); - } - setSelectedElements(items: IQuickPickItem[]) { const elements = items.map(item => this._itemElements.find(e => e.item === item)) .filter((e): e is QuickPickItemElement => !!e); @@ -1195,55 +1173,69 @@ export class QuickInputTree extends Disposable { } setCheckedElements(items: IQuickPickItem[]) { - try { - this._shouldFireCheckedEvents = false; + this._elementCheckedEventBufferer.bufferEvents(() => { const checked = new Set(); for (const item of items) { checked.add(item); } for (const element of this._itemElements) { - // Would fire an event if we didn't have the flag set + // Would fire an event if we didn't beffer the events element.checked = checked.has(element.item); } - } finally { - this._shouldFireCheckedEvents = true; - this._fireCheckedEvents(); - } + }); } - focus(what: QuickInputListFocus): void { + focus(what: QuickPickFocus): void { if (!this._itemElements.length) { return; } - if (what === QuickInputListFocus.Second && this._itemElements.length < 2) { - what = QuickInputListFocus.First; + if (what === QuickPickFocus.Second && this._itemElements.length < 2) { + what = QuickPickFocus.First; } switch (what) { - case QuickInputListFocus.First: + case QuickPickFocus.First: this._tree.scrollTop = 0; this._tree.focusFirst(undefined, (e) => e.element instanceof QuickPickItemElement); break; - case QuickInputListFocus.Second: + case QuickPickFocus.Second: { this._tree.scrollTop = 0; - this._tree.setFocus([this._itemElements[1]]); + let isSecondItem = false; + this._tree.focusFirst(undefined, (e) => { + if (!(e.element instanceof QuickPickItemElement)) { + return false; + } + if (isSecondItem) { + return true; + } + isSecondItem = !isSecondItem; + return false; + }); break; - case QuickInputListFocus.Last: + } + case QuickPickFocus.Last: this._tree.scrollTop = this._tree.scrollHeight; - this._tree.setFocus([this._itemElements[this._itemElements.length - 1]]); + this._tree.focusLast(undefined, (e) => e.element instanceof QuickPickItemElement); break; - case QuickInputListFocus.Next: - this._tree.focusNext(undefined, true, undefined, (e) => { + case QuickPickFocus.Next: { + const prevFocus = this._tree.getFocus(); + this._tree.focusNext(undefined, this._shouldLoop, undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; } this._tree.reveal(e.element); return true; }); + const currentFocus = this._tree.getFocus(); + if (prevFocus.length && prevFocus[0] === currentFocus[0] && prevFocus[0] === this._itemElements[this._itemElements.length - 1]) { + this._onLeave.fire(); + } break; - case QuickInputListFocus.Previous: - this._tree.focusPrevious(undefined, true, undefined, (e) => { + } + case QuickPickFocus.Previous: { + const prevFocus = this._tree.getFocus(); + this._tree.focusPrevious(undefined, this._shouldLoop, undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; } @@ -1256,8 +1248,13 @@ export class QuickInputTree extends Disposable { } return true; }); + const currentFocus = this._tree.getFocus(); + if (prevFocus.length && prevFocus[0] === currentFocus[0] && prevFocus[0] === this._itemElements[0]) { + this._onLeave.fire(); + } break; - case QuickInputListFocus.NextPage: + } + case QuickPickFocus.NextPage: this._tree.focusNextPage(undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; @@ -1266,7 +1263,7 @@ export class QuickInputTree extends Disposable { return true; }); break; - case QuickInputListFocus.PreviousPage: + case QuickPickFocus.PreviousPage: this._tree.focusPreviousPage(undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; @@ -1280,7 +1277,7 @@ export class QuickInputTree extends Disposable { return true; }); break; - case QuickInputListFocus.NextSeparator: { + case QuickPickFocus.NextSeparator: { let foundSeparatorAsItem = false; const before = this._tree.getFocus()[0]; this._tree.focusNext(undefined, true, undefined, (e) => { @@ -1292,9 +1289,9 @@ export class QuickInputTree extends Disposable { if (e.element instanceof QuickPickSeparatorElement) { foundSeparatorAsItem = true; - // If the separator is visible, then we should just focus it. + // If the separator is visible, then we should just reveal its first child so it's not as jarring. if (this._separatorRenderer.isSeparatorVisible(e.element)) { - this._tree.reveal(e.element); + this._tree.reveal(e.element.children[0]); } else { // If the separator is not visible, then we should // push it up to the top of the list. @@ -1321,11 +1318,11 @@ export class QuickInputTree extends Disposable { // If we didn't move, then we should just move to the end // of the list. this._tree.scrollTop = this._tree.scrollHeight; - this._tree.setFocus([this._itemElements[this._itemElements.length - 1]]); + this._tree.focusLast(undefined, (e) => e.element instanceof QuickPickItemElement); } break; } - case QuickInputListFocus.PreviousSeparator: { + case QuickPickFocus.PreviousSeparator: { let focusElement: IQuickPickElement | undefined; // If we are already sitting on an inline separator, then we // have already found the _current_ separator and need to @@ -1391,6 +1388,7 @@ export class QuickInputTree extends Disposable { } filter(query: string): boolean { + this._lastQueryString = query; if (!(this._sortByLabel || this._matchOnLabel || this._matchOnDescription || this._matchOnDetail)) { this._tree.layout(); return false; @@ -1416,7 +1414,7 @@ export class QuickInputTree extends Disposable { // Filter by value (since we support icons in labels, use $(..) aware fuzzy matching) else { let currentSeparator: IQuickPickSeparator | undefined; - this._elementTree.forEach(element => { + this._itemElements.forEach(element => { let labelHighlights: IMatch[] | undefined; if (this.matchOnLabelMode === 'fuzzy') { labelHighlights = this.matchOnLabel ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel)) ?? undefined : undefined; @@ -1447,8 +1445,10 @@ export class QuickInputTree extends Disposable { // we can show the separator unless the list gets sorted by match if (!this.sortByLabel) { - const previous = element.index && this._inputElements[element.index - 1]; - currentSeparator = previous && previous.type === 'separator' ? previous : currentSeparator; + const previous = element.index && this._inputElements[element.index - 1] || undefined; + if (previous?.type === 'separator' && !previous.buttons) { + currentSeparator = previous; + } if (currentSeparator && !element.hidden) { element.separator = currentSeparator; currentSeparator = undefined; @@ -1457,65 +1457,18 @@ export class QuickInputTree extends Disposable { }); } - const shownElements = this._elementTree.filter(element => !element.hidden); - - // Sort by value - if (this.sortByLabel && query) { - const normalizedSearchValue = query.toLowerCase(); - shownElements.sort((a, b) => { - return compareEntries(a, b, normalizedSearchValue); - }); - } - - let currentSeparator: QuickPickSeparatorElement | undefined; - const finalElements = shownElements.reduce((result, element, index) => { - if (element instanceof QuickPickItemElement) { - if (currentSeparator) { - currentSeparator.children.push(element); - } else { - result.push(element); - } - } else if (element instanceof QuickPickSeparatorElement) { - element.children = []; - currentSeparator = element; - result.push(element); - } - return result; - }, new Array()); - - const elements = new Array>(); - for (const element of finalElements) { - if (element instanceof QuickPickSeparatorElement) { - elements.push({ - element, - collapsible: false, - collapsed: false, - children: element.children.map(e => ({ - element: e, - collapsible: false, - collapsed: false, - })), - }); - } else { - elements.push({ - element, - collapsible: false, - collapsed: false, - }); - } - } - this._tree.setChildren(null, elements); + this._setElementsToTree(this._sortByLabel && query + // We don't render any separators if we're sorting so just render the elements + ? this._itemElements + // Render the full tree + : this._elementTree + ); this._tree.layout(); - - this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); - this._onChangedVisibleCount.fire(shownElements.length); - return true; } toggleCheckbox() { - try { - this._shouldFireCheckedEvents = false; + this._elementCheckedEventBufferer.bufferEvents(() => { const elements = this._tree.getFocus().filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement); const allChecked = this._allVisibleChecked(elements); for (const element of elements) { @@ -1524,18 +1477,7 @@ export class QuickInputTree extends Disposable { element.checked = !allChecked; } } - } finally { - this._shouldFireCheckedEvents = true; - this._fireCheckedEvents(); - } - } - - display(display: boolean) { - this._container.style.display = display ? '' : 'none'; - } - - isDisplayed() { - return this._container.style.display !== 'none'; + }); } style(styles: IListStyles) { @@ -1572,6 +1514,31 @@ export class QuickInputTree extends Disposable { //#region private methods + private _setElementsToTree(elements: IQuickPickElement[]) { + const treeElements = new Array>(); + for (const element of elements) { + if (element instanceof QuickPickSeparatorElement) { + treeElements.push({ + element, + collapsible: false, + collapsed: false, + children: element.children.map(e => ({ + element: e, + collapsible: false, + collapsed: false, + })), + }); + } else { + treeElements.push({ + element, + collapsible: false, + collapsed: false, + }); + } + } + this._tree.setChildren(null, treeElements); + } + private _allVisibleChecked(elements: QuickPickItemElement[], whenNoneVisible = true) { for (let i = 0, n = elements.length; i < n; i++) { const element = elements[i]; @@ -1586,21 +1553,13 @@ export class QuickInputTree extends Disposable { return whenNoneVisible; } - private _fireCheckedEvents() { - if (!this._shouldFireCheckedEvents) { - return; - } - this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); - this._onChangedCheckedCount.fire(this.getCheckedCount()); - this._onChangedCheckedElements.fire(this.getCheckedElements()); - } - - private fireButtonTriggered(event: IQuickPickItemButtonEvent) { - this._onButtonTriggered.fire(event); - } - - private fireSeparatorButtonTriggered(event: IQuickPickSeparatorButtonEvent) { - this._onSeparatorButtonTriggered.fire(event); + private _updateCheckedObservables() { + transaction((tx) => { + this._allVisibleCheckedObservable.set(this._allVisibleChecked(this._itemElements, false), tx); + const checkedCount = this._itemElements.filter(element => element.checked).length; + this._checkedCountObservable.set(checkedCount, tx); + this._checkedElementsObservable.set(this.getCheckedElements(), tx); + }); } /** diff --git a/src/vs/platform/quickinput/browser/quickPickPin.ts b/src/vs/platform/quickinput/browser/quickPickPin.ts index 51c8b25d467..d7141ef5f29 100644 --- a/src/vs/platform/quickinput/browser/quickPickPin.ts +++ b/src/vs/platform/quickinput/browser/quickPickPin.ts @@ -18,7 +18,7 @@ const buttonClasses = [pinButtonClass, pinnedButtonClass]; * be removed if @param filterDupliates has been provided. Pin and pinned button events trigger updates to the underlying storage. * Shows the quickpick once formatted. */ -export async function showWithPinnedItems(storageService: IStorageService, storageKey: string, quickPick: IQuickPick, filterDuplicates?: boolean): Promise { +export async function showWithPinnedItems(storageService: IStorageService, storageKey: string, quickPick: IQuickPick, filterDuplicates?: boolean): Promise { const itemsWithoutPinned = quickPick.items; let itemsWithPinned = _formatPinnedItems(storageKey, quickPick, storageService, undefined, filterDuplicates); quickPick.onDidTriggerItemButton(async buttonEvent => { @@ -41,7 +41,7 @@ export async function showWithPinnedItems(storageService: IStorageService, stora quickPick.show(); } -function _formatPinnedItems(storageKey: string, quickPick: IQuickPick, storageService: IStorageService, changedItem?: IQuickPickItem, filterDuplicates?: boolean): QuickPickItem[] { +function _formatPinnedItems(storageKey: string, quickPick: IQuickPick, storageService: IStorageService, changedItem?: IQuickPickItem, filterDuplicates?: boolean): QuickPickItem[] { const formattedItems: QuickPickItem[] = []; let pinnedItems; if (changedItem) { diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index c160bb1fb93..987571327d0 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -15,6 +15,13 @@ import { Registry } from 'vs/platform/registry/common/platform'; */ export interface IQuickAccessProviderRunOptions { readonly from?: string; + readonly placeholder?: string; + /** + * A handler to invoke when an item is accepted for + * this particular showing of the quick access. + * @param item The item that was accepted. + */ + readonly handleAccept?: (item: IQuickPickItem) => void; } /** @@ -22,6 +29,7 @@ export interface IQuickAccessProviderRunOptions { */ export interface AnythingQuickAccessProviderRunOptions extends IQuickAccessProviderRunOptions { readonly includeHelp?: boolean; + readonly filter?: (item: unknown) => boolean; /** * @deprecated - temporary for Dynamic Chat Variables (see usage) until it has built-in UX for file picking * Useful for adding items to the top of the list that might contain actions. @@ -53,6 +61,17 @@ export interface IQuickAccessOptions { * quick access. */ readonly providerOptions?: IQuickAccessProviderRunOptions; + + /** + * An array of provider prefixes to enable for this + * particular showing of the quick access. + */ + readonly enabledProviderPrefixes?: string[]; + + /** + * A placeholder to use for this particular showing of the quick access. + */ + readonly placeholder?: string; } export interface IQuickAccessController { @@ -110,7 +129,7 @@ export interface IQuickAccessProvider { * @return a disposable that will automatically be disposed when the picker * closes or is replaced by another picker. */ - provide(picker: IQuickPick, token: CancellationToken, options?: IQuickAccessProviderRunOptions): IDisposable; + provide(picker: IQuickPick, token: CancellationToken, options?: IQuickAccessProviderRunOptions): IDisposable; } export interface IQuickAccessProviderHelp { diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 820544bcd69..9dcd5049d7b 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -204,11 +204,25 @@ export interface IQuickInputHideEvent { reason: QuickInputHideReason; } +/** + * A collection of the different types of QuickInput + */ +export const enum QuickInputType { + QuickPick = 'quickPick', + InputBox = 'inputBox', + QuickWidget = 'quickWidget' +} + /** * Represents a quick input control that allows users to make selections or provide input quickly. */ export interface IQuickInput extends IDisposable { + /** + * The type of the quick input. + */ + readonly type: QuickInputType; + /** * An event that is fired when the quick input is hidden. */ @@ -304,6 +318,12 @@ export interface IQuickInput extends IDisposable { } export interface IQuickWidget extends IQuickInput { + + /** + * The type of the quick input. + */ + readonly type: QuickInputType.QuickWidget; + /** * Should be an HTMLElement (TODO: move this entire file into browser) * @override @@ -353,10 +373,57 @@ export enum ItemActivation { LAST } +/** + * Represents the focus options for a quick pick. + */ +export enum QuickPickFocus { + /** + * Focus the first item in the list. + */ + First = 1, + /** + * Focus the second item in the list. + */ + Second, + /** + * Focus the last item in the list. + */ + Last, + /** + * Focus the next item in the list. + */ + Next, + /** + * Focus the previous item in the list. + */ + Previous, + /** + * Focus the next page in the list. + */ + NextPage, + /** + * Focus the previous page in the list. + */ + PreviousPage, + /** + * Focus the first item under the next separator. + */ + NextSeparator, + /** + * Focus the first item under the current separator. + */ + PreviousSeparator +} + /** * Represents a quick pick control that allows the user to select an item from a list of options. */ -export interface IQuickPick extends IQuickInput { +export interface IQuickPick extends IQuickInput { + + /** + * The type of the quick input. + */ + readonly type: QuickInputType.QuickPick; /** * The current value of the quick pick input. @@ -438,7 +505,7 @@ export interface IQuickPick extends IQuickInput { /** * The items to be displayed in the quick pick. */ - items: ReadonlyArray; + items: O extends { useSeparators: true } ? ReadonlyArray : ReadonlyArray; /** * Whether multiple items can be selected. If so, checkboxes will be rendered. @@ -556,6 +623,18 @@ export interface IQuickPick extends IQuickInput { * The toggle buttons to be added to the input box. */ toggles: IQuickInputToggle[] | undefined; + + /** + * Focus a particular item in the list. Used internally for keyboard navigation. + * @param focus The focus behavior. + */ + focus(focus: QuickPickFocus): void; + + /** + * Programmatically accepts an item. Used internally for keyboard navigation. + * @param inBackground Whether you are accepting an item in the background and keeping the picker open. + */ + accept(inBackground?: boolean): void; } /** @@ -574,6 +653,11 @@ export interface IQuickInputToggle { */ export interface IInputBox extends IQuickInput { + /** + * The type of the quick input. + */ + readonly type: QuickInputType.InputBox; + /** * Value shown in the input box. */ @@ -621,6 +705,18 @@ export interface IInputBox extends IQuickInput { severity: Severity; } +export enum QuickInputButtonLocation { + /** + * In the title bar. + */ + Title = 1, + + /** + * To the right of the input box. + */ + Inline = 2 +} + /** * Represents a button in the quick input UI. */ @@ -644,6 +740,11 @@ export interface IQuickInputButton { * By default, buttons are only visible when hovering over them with the mouse. */ alwaysVisible?: boolean; + /** + * Where the button should be rendered. The default is {@link QuickInputButtonLocation.Title}. + * @note This property is ignored if the button was added to a QuickPickItem. + */ + location?: QuickInputButtonLocation; } /** @@ -770,7 +871,8 @@ export interface IQuickInputService { /** * Provides raw access to the quick pick controller. */ - createQuickPick(): IQuickPick; + createQuickPick(options: { useSeparators: true }): IQuickPick; + createQuickPick(options?: { useSeparators: boolean }): IQuickPick; /** * Provides raw access to the input box controller. @@ -814,4 +916,9 @@ export interface IQuickInputService { * Cancels quick input and closes it. */ cancel(): Promise; + + /** + * The current quick pick that is visible. Undefined if none is open. + */ + currentQuickInput: IQuickInput | undefined; } diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 216aa3a7f54..328c43d72fb 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; @@ -32,6 +32,8 @@ import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyServ import { NoMatchingKb } from 'vs/platform/keybinding/common/keybindingResolver'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; // Sets up an `onShow` listener to allow us to wait until the quick pick is shown (useful when triggering an `accept()` right after launching a quick pick) // kick this off before you launch the picker and then await the promise returned after you launch the picker. @@ -55,13 +57,14 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 setup(() => { const fixture = document.createElement('div'); mainWindow.document.body.appendChild(fixture); - store.add(toDisposable(() => mainWindow.document.body.removeChild(fixture))); + store.add(toDisposable(() => fixture.remove())); const instantiationService = new TestInstantiationService(); // Stub the services the quick input controller needs to function instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); instantiationService.stub(IListService, store.add(new ListService())); instantiationService.stub(ILayoutService, { activeContainer: fixture, onDidLayoutContainer: Event.None } as any); instantiationService.stub(IContextViewService, store.add(instantiationService.createInstance(ContextViewService))); diff --git a/src/vs/platform/registry/test/common/platform.test.ts b/src/vs/platform/registry/test/common/platform.test.ts index 3fe9188fa8d..8ec965d503b 100644 --- a/src/vs/platform/registry/test/common/platform.test.ts +++ b/src/vs/platform/registry/test/common/platform.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isFunction } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/platform/remote/common/remoteExtensionsScanner.ts b/src/vs/platform/remote/common/remoteExtensionsScanner.ts index 792c0352bb7..a26de9d171b 100644 --- a/src/vs/platform/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/platform/remote/common/remoteExtensionsScanner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -16,5 +15,4 @@ export interface IRemoteExtensionsScannerService { whenExtensionsReady(): Promise; scanExtensions(): Promise; - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise; } diff --git a/src/vs/platform/remote/node/wsl.ts b/src/vs/platform/remote/node/wsl.ts index 3ba33f74b96..637ccff4fb0 100644 --- a/src/vs/platform/remote/node/wsl.ts +++ b/src/vs/platform/remote/node/wsl.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as os from 'os'; import * as cp from 'child_process'; -import { Promises } from 'vs/base/node/pfs'; import * as path from 'path'; let hasWSLFeaturePromise: Promise | undefined; @@ -26,14 +26,18 @@ async function testWSLFeatureInstalled(): Promise { const wslExePath = getWSLExecutablePath(); if (wslExePath) { return new Promise(s => { - cp.execFile(wslExePath, ['--status'], err => s(!err)); + try { + cp.execFile(wslExePath, ['--status'], err => s(!err)); + } catch (e) { + s(false); + } }); } } else { const dllPath = getLxssManagerDllPath(); if (dllPath) { try { - if ((await Promises.stat(dllPath)).isFile()) { + if ((await fs.promises.stat(dllPath)).isFile()) { return true; } } catch (e) { diff --git a/src/vs/platform/remote/test/common/remoteHosts.test.ts b/src/vs/platform/remote/test/common/remoteHosts.test.ts index 0f2254b8b87..ed564551df9 100644 --- a/src/vs/platform/remote/test/common/remoteHosts.test.ts +++ b/src/vs/platform/remote/test/common/remoteHosts.test.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseAuthorityWithOptionalPort, parseAuthorityWithPort } from 'vs/platform/remote/common/remoteHosts'; suite('remoteHosts', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('parseAuthority hostname', () => { assert.deepStrictEqual(parseAuthorityWithPort('localhost:8080'), { host: 'localhost', port: 8080 }); }); diff --git a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts index a5bacbb8c86..67790a807c6 100644 --- a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts +++ b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; diff --git a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts index 22c7d83535a..d0d433a33e6 100644 --- a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts @@ -26,8 +26,8 @@ import { joinPath } from 'vs/base/common/resources'; type RemoteTunnelEnablementClassification = { owner: 'aeschli'; comment: 'Reporting when Remote Tunnel access is turned on or off'; - enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if Remote Tunnel Access is enabled or not' }; - service?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if Remote Tunnel Access is installed as a service' }; + enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if Remote Tunnel Access is enabled or not' }; + service?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if Remote Tunnel Access is installed as a service' }; }; type RemoteTunnelEnablementEvent = { @@ -306,7 +306,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ a = a.replaceAll(token, '*'.repeat(4)); onOutput(a, isErr); }; - const loginProcess = this.runCodeTunnelCommand('login', ['user', 'login', '--provider', session.providerId, '--access-token', token, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput); + const loginProcess = this.runCodeTunnelCommand('login', ['user', 'login', '--provider', session.providerId, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput, { VSCODE_CLI_ACCESS_TOKEN: token }); this._tunnelProcess = loginProcess; try { await loginProcess; @@ -408,7 +408,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ }); } - private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput): CancelablePromise { + private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput, env?: Record): CancelablePromise { return createCancelablePromise(token => { return new Promise((resolve, reject) => { if (token.isCancellationRequested) { @@ -426,12 +426,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ if (!this.environmentService.isBuilt) { onOutput('Building tunnel CLI from sources and run\n', false); onOutput(`${logLabel} Spawning: cargo run -- tunnel ${commandArgs.join(' ')}\n`, false); - tunnelProcess = spawn('cargo', ['run', '--', 'tunnel', ...commandArgs], { cwd: join(this.environmentService.appRoot, 'cli'), stdio }); + tunnelProcess = spawn('cargo', ['run', '--', 'tunnel', ...commandArgs], { cwd: join(this.environmentService.appRoot, 'cli'), stdio, env: { ...process.env, RUST_BACKTRACE: '1', ...env } }); } else { onOutput('Running tunnel CLI\n', false); const tunnelCommand = this.getTunnelCommandLocation(); onOutput(`${logLabel} Spawning: ${tunnelCommand} tunnel ${commandArgs.join(' ')}\n`, false); - tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio }); + tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio, env: { ...process.env, ...env } }); } tunnelProcess.stdout!.pipe(new StreamSplitter('\n')).on('data', data => { diff --git a/src/vs/platform/request/browser/requestService.ts b/src/vs/platform/request/browser/requestService.ts index f0fc3d68790..78c34491ff2 100644 --- a/src/vs/platform/request/browser/requestService.ts +++ b/src/vs/platform/request/browser/requestService.ts @@ -8,7 +8,7 @@ import { request } from 'vs/base/parts/request/browser/request'; import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILoggerService } from 'vs/platform/log/common/log'; -import { AbstractRequestService, IRequestService } from 'vs/platform/request/common/request'; +import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; /** * This service exposes the `request` API, while using the global @@ -36,6 +36,14 @@ export class RequestService extends AbstractRequestService implements IRequestSe return undefined; // not implemented in the web } + async lookupAuthorization(authInfo: AuthInfo): Promise { + return undefined; // not implemented in the web + } + + async lookupKerberosAuthorization(url: string): Promise { + return undefined; // not implemented in the web + } + async loadCertificates(): Promise { return []; // not implemented in the web } diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 289ad6740e0..fb732b7f772 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -16,12 +16,28 @@ import { Registry } from 'vs/platform/registry/common/platform'; export const IRequestService = createDecorator('requestService'); +export interface AuthInfo { + isProxy: boolean; + scheme: string; + host: string; + port: number; + realm: string; + attempt: number; +} + +export interface Credentials { + username: string; + password: string; +} + export interface IRequestService { readonly _serviceBrand: undefined; request(options: IRequestOptions, token: CancellationToken): Promise; resolveProxy(url: string): Promise; + lookupAuthorization(authInfo: AuthInfo): Promise; + lookupKerberosAuthorization(url: string): Promise; loadCertificates(): Promise; } @@ -80,6 +96,8 @@ export abstract class AbstractRequestService extends Disposable implements IRequ abstract request(options: IRequestOptions, token: CancellationToken): Promise; abstract resolveProxy(url: string): Promise; + abstract lookupAuthorization(authInfo: AuthInfo): Promise; + abstract lookupKerberosAuthorization(url: string): Promise; abstract loadCertificates(): Promise; } @@ -155,6 +173,12 @@ function registerProxyConfigurations(scope: ConfigurationScope): void { markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."), restricted: true }, + 'http.noProxy': { + type: 'array', + items: { type: 'string' }, + markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests."), + restricted: true + }, 'http.proxyAuthorization': { type: ['null', 'string'], default: null, diff --git a/src/vs/platform/request/common/requestIpc.ts b/src/vs/platform/request/common/requestIpc.ts index 421106ebff6..f6854f5e8f6 100644 --- a/src/vs/platform/request/common/requestIpc.ts +++ b/src/vs/platform/request/common/requestIpc.ts @@ -8,7 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; -import { IRequestService } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; type RequestResponse = [ { @@ -34,6 +34,8 @@ export class RequestChannel implements IServerChannel { return [{ statusCode: res.statusCode, headers: res.headers }, buffer]; }); case 'resolveProxy': return this.service.resolveProxy(args[0]); + case 'lookupAuthorization': return this.service.lookupAuthorization(args[0]); + case 'lookupKerberosAuthorization': return this.service.lookupKerberosAuthorization(args[0]); case 'loadCertificates': return this.service.loadCertificates(); } throw new Error('Invalid call'); @@ -55,6 +57,14 @@ export class RequestChannelClient implements IRequestService { return this.channel.call('resolveProxy', [url]); } + async lookupAuthorization(authInfo: AuthInfo): Promise { + return this.channel.call<{ username: string; password: string } | undefined>('lookupAuthorization', [authInfo]); + } + + async lookupKerberosAuthorization(url: string): Promise { + return this.channel.call('lookupKerberosAuthorization', [url]); + } + async loadCertificates(): Promise { return this.channel.call('loadCertificates'); } diff --git a/src/vs/platform/request/node/proxy.ts b/src/vs/platform/request/node/proxy.ts index db448e06cc1..141be5bb796 100644 --- a/src/vs/platform/request/node/proxy.ts +++ b/src/vs/platform/request/node/proxy.ts @@ -44,7 +44,21 @@ export async function getProxyAgent(rawRequestURL: string, env: typeof process.e rejectUnauthorized: isBoolean(options.strictSSL) ? options.strictSSL : true, }; - return requestURL.protocol === 'http:' - ? new (await import('http-proxy-agent')).HttpProxyAgent(proxyURL, opts) - : new (await import('https-proxy-agent')).HttpsProxyAgent(proxyURL, opts); + if (requestURL.protocol === 'http:') { + // ESM-comment-begin + const mod = await import('http-proxy-agent'); + // ESM-comment-end + // ESM-uncomment-begin + // const mod = (await import('http-proxy-agent')).default; + // ESM-uncomment-end + return new mod.HttpProxyAgent(proxyURL, opts); + } else { + // ESM-comment-begin + const mod = await import('https-proxy-agent'); + // ESM-comment-end + // ESM-uncomment-begin + // const mod = (await import('https-proxy-agent')).default; + // ESM-uncomment-end + return new mod.HttpsProxyAgent(proxyURL, opts); + } } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 23f8f0d44c8..f3c57ebbf5c 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { ILogService, ILoggerService } from 'vs/platform/log/common/log'; -import { AbstractRequestService, IRequestService } from 'vs/platform/request/common/request'; +import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; import { Agent, getProxyAgent } from 'vs/platform/request/node/proxy'; import { createGunzip } from 'zlib'; @@ -110,6 +110,26 @@ export class RequestService extends AbstractRequestService implements IRequestSe return undefined; // currently not implemented in node } + async lookupAuthorization(authInfo: AuthInfo): Promise { + return undefined; // currently not implemented in node + } + + async lookupKerberosAuthorization(urlStr: string): Promise { + try { + const kerberos = await import('kerberos'); + const url = new URL(urlStr); + const spn = this.configurationService.getValue('http.proxyKerberosServicePrincipal') + || (process.platform === 'win32' ? `HTTP/${url.hostname}` : `HTTP@${url.hostname}`); + this.logService.debug('RequestService#lookupKerberosAuthorization Kerberos authentication lookup', `proxyURL:${url}`, `spn:${spn}`); + const client = await kerberos.initializeClient(spn); + const response = await client.step(''); + return 'Negotiate ' + response; + } catch (err) { + this.logService.debug('RequestService#lookupKerberosAuthorization Kerberos authentication failed', err); + return undefined; + } + } + async loadCertificates(): Promise { const proxyAgent = await import('@vscode/proxy-agent'); return proxyAgent.loadSystemCertificates({ log: this.logService }); @@ -165,7 +185,7 @@ export async function nodeRequest(options: NodeRequestOptions, token: Cancellati stream = res.pipe(createGunzip()); } - resolve({ res, stream: streamToBufferReadableStream(stream) } as IRequestContext); + resolve({ res, stream: streamToBufferReadableStream(stream) } satisfies IRequestContext); } }); diff --git a/src/vs/platform/secrets/test/common/secrets.test.ts b/src/vs/platform/secrets/test/common/secrets.test.ts index b3a048af2f2..50def3a9a92 100644 --- a/src/vs/platform/secrets/test/common/secrets.test.ts +++ b/src/vs/platform/secrets/test/common/secrets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEncryptionService, KnownStorageProvider } from 'vs/platform/encryption/common/encryptionService'; diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 087f5858b48..5cb80e342cc 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -19,6 +19,7 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { parseSharedProcessDebugPort } from 'vs/platform/environment/node/environmentService'; import { assertIsDefined } from 'vs/base/common/types'; import { SharedProcessChannelConnection, SharedProcessRawConnection, SharedProcessLifecycle } from 'vs/platform/sharedProcess/common/sharedProcess'; +import { Emitter } from 'vs/base/common/event'; export class SharedProcess extends Disposable { @@ -27,9 +28,13 @@ export class SharedProcess extends Disposable { private utilityProcess: UtilityProcess | undefined = undefined; private utilityProcessLogListener: IDisposable | undefined = undefined; + private readonly _onDidCrash = this._register(new Emitter()); + readonly onDidCrash = this._onDidCrash.event; + constructor( private readonly machineId: string, private readonly sqmId: string, + private readonly devDeviceId: string, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @@ -168,12 +173,15 @@ export class SharedProcess extends Disposable { payload: this.createSharedProcessConfiguration(), execArgv }); + + this._register(this.utilityProcess.onCrash(() => this._onDidCrash.fire())); } private createSharedProcessConfiguration(): ISharedProcessConfiguration { return { machineId: this.machineId, sqmId: this.sqmId, + devDeviceId: this.devDeviceId, codeCachePath: this.environmentMainService.codeCachePath, profiles: { home: this.userDataProfilesService.profilesHome, diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts index f93082d7a2d..0c0e49c3a0b 100644 --- a/src/vs/platform/sharedProcess/node/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -15,6 +15,8 @@ export interface ISharedProcessConfiguration { readonly sqmId: string; + readonly devDeviceId: string; + readonly codeCachePath: string | undefined; readonly args: NativeParsedArgs; diff --git a/src/vs/platform/sign/browser/signService.ts b/src/vs/platform/sign/browser/signService.ts index a9d699bf3b2..b6b08ebf466 100644 --- a/src/vs/platform/sign/browser/signService.ts +++ b/src/vs/platform/sign/browser/signService.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { importAMDNodeModule, resolveAmdNodeModulePath } from 'vs/amdX'; import { WindowIntervalTimer } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isESM } from 'vs/base/common/amd'; import { memoize } from 'vs/base/common/decorators'; import { FileAccess } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -62,7 +64,7 @@ export class SignService extends AbstractSignService implements ISignService { let [wasm] = await Promise.all([ this.getWasmBytes(), new Promise((resolve, reject) => { - require(['vsda'], resolve, reject); + importAMDNodeModule('vsda', 'rust/web/vsda.js').then(() => resolve(), reject); // todo@connor4312: there seems to be a bug(?) in vscode-loader with // require() not resolving in web once the script loads, so check manually @@ -74,7 +76,6 @@ export class SignService extends AbstractSignService implements ISignService { }).finally(() => checkInterval.dispose()), ]); - const keyBytes = new TextEncoder().encode(this.productService.serverLicense?.join('\n') || ''); for (let i = 0; i + STEP_SIZE < keyBytes.length; i += STEP_SIZE) { const key = await crypto.subtle.importKey('raw', keyBytes.slice(i + IV_SIZE, i + IV_SIZE + KEY_SIZE), { name: 'AES-CBC' }, false, ['decrypt']); @@ -87,7 +88,10 @@ export class SignService extends AbstractSignService implements ISignService { } private async getWasmBytes(): Promise { - const response = await fetch(FileAccess.asBrowserUri('vsda/../vsda_bg.wasm').toString(true)); + const url = isESM + ? resolveAmdNodeModulePath('vsda', 'rust/web/vsda_bg.wasm') + : FileAccess.asBrowserUri('vsda/../vsda_bg.wasm').toString(true); + const response = await fetch(url); if (!response.ok) { throw new Error('error loading vsda'); } diff --git a/src/vs/platform/sign/common/abstractSignService.ts b/src/vs/platform/sign/common/abstractSignService.ts index 6f7c91ba958..838dde91518 100644 --- a/src/vs/platform/sign/common/abstractSignService.ts +++ b/src/vs/platform/sign/common/abstractSignService.ts @@ -36,7 +36,7 @@ export abstract class AbstractSignService implements ISignService { }; } } catch (e) { - // ignore errors silently + console.error(e); } return { id: '', data: value }; } @@ -54,7 +54,7 @@ export abstract class AbstractSignService implements ISignService { try { return (validator.validate(value) === 'ok'); } catch (e) { - // ignore errors silently + console.error(e); return false; } finally { validator.dispose?.(); @@ -65,7 +65,7 @@ export abstract class AbstractSignService implements ISignService { try { return await this.signValue(value); } catch (e) { - // ignore errors silently + console.error(e); } return value; } diff --git a/src/vs/platform/sign/node/signService.ts b/src/vs/platform/sign/node/signService.ts index d07ba9cfbe9..1a2023d6ad1 100644 --- a/src/vs/platform/sign/node/signService.ts +++ b/src/vs/platform/sign/node/signService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { importAMDNodeModule } from 'vs/amdX'; import { AbstractSignService, IVsdaValidator } from 'vs/platform/sign/common/abstractSignService'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -29,6 +30,6 @@ export class SignService extends AbstractSignService implements ISignService { } private vsda(): Promise { - return new Promise((resolve, reject) => require(['vsda'], resolve, reject)); + return importAMDNodeModule('vsda', 'index.js'); } } diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index 493d78d0e51..4674f20a4ee 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { readFileSync } from 'fs'; +import assert from 'assert'; +import { readFileSync, promises } from 'fs'; import { tmpdir } from 'os'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; @@ -37,7 +37,7 @@ flakySuite('StateService', () => { diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService)); disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider)); - return Promises.mkdir(testDir, { recursive: true }); + return promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index c110f28015b..8d6d1b539d5 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { top } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -403,7 +404,7 @@ export class WorkspaceStorageMain extends BaseStorageMain { } // Ensure storage folder exists - await Promises.mkdir(workspaceStorageFolderPath, { recursive: true }); + await fs.promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder (but do not await) this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); diff --git a/src/vs/platform/telemetry/common/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts index 9ae3427a07b..b649cb80775 100644 --- a/src/vs/platform/telemetry/common/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -24,6 +24,7 @@ export function resolveCommonProperties( version: string | undefined, machineId: string | undefined, sqmId: string | undefined, + devDeviceId: string | undefined, isInternalTelemetry: boolean, product?: string ): ICommonProperties { @@ -33,6 +34,8 @@ export function resolveCommonProperties( result['common.machineId'] = machineId; // __GDPR__COMMON__ "common.sqmId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } result['common.sqmId'] = sqmId; + // __GDPR__COMMON__ "common.devDeviceId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } + result['common.devDeviceId'] = devDeviceId; // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['sessionID'] = generateUuid() + Date.now(); // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } diff --git a/src/vs/platform/telemetry/common/errorTelemetry.ts b/src/vs/platform/telemetry/common/errorTelemetry.ts index 22c005fe2a6..a872cbb4dee 100644 --- a/src/vs/platform/telemetry/common/errorTelemetry.ts +++ b/src/vs/platform/telemetry/common/errorTelemetry.ts @@ -16,11 +16,11 @@ type ErrorEventFragment = { callstack: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The callstack of the error.' }; msg?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The message of the error. Normally the first line int the callstack.' }; file?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The file the error originated from.' }; - line?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The line the error originate on.' }; - column?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The column of the line which the error orginated on.' }; + line?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The line the error originate on.' }; + column?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The column of the line which the error orginated on.' }; uncaught_error_name?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'If the error is uncaught what is the error type' }; uncaught_error_msg?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'If the error is uncaught this is just msg but for uncaught errors.' }; - count?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'How many times this error has been thrown' }; + count?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'How many times this error has been thrown' }; }; export interface ErrorEvent { callstack: string; diff --git a/src/vs/platform/telemetry/common/gdprTypings.ts b/src/vs/platform/telemetry/common/gdprTypings.ts index 903c0e698f7..20638f84273 100644 --- a/src/vs/platform/telemetry/common/gdprTypings.ts +++ b/src/vs/platform/telemetry/common/gdprTypings.ts @@ -8,7 +8,6 @@ export interface IPropertyData { comment: string; expiration?: string; endpoint?: string; - isMeasurement?: boolean; } export interface IGDPRProperty { diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 73db745352a..d6b4179b71f 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -23,6 +23,7 @@ export interface ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; @@ -73,6 +74,7 @@ export const firstSessionDateStorageKey = 'telemetry.firstSessionDate'; export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; export const machineIdKey = 'telemetry.machineId'; export const sqmIdKey = 'telemetry.sqmId'; +export const devDeviceIdKey = 'telemetry.devDeviceId'; // Configuration Keys export const TELEMETRY_SECTION_ID = 'telemetry'; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index a9a9cc48109..245bb41b5d8 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -34,6 +34,7 @@ export class TelemetryService implements ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal: boolean | undefined; @@ -58,6 +59,7 @@ export class TelemetryService implements ITelemetryService { this.sessionId = this._commonProperties['sessionID'] as string; this.machineId = this._commonProperties['common.machineId'] as string; this.sqmId = this._commonProperties['common.sqmId'] as string; + this.devDeviceId = this._commonProperties['common.devDeviceId'] as string; this.firstSessionDate = this._commonProperties['common.firstSessionDate'] as string; this.msftInternal = this._commonProperties['common.msftInternal'] as boolean | undefined; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 5deb494c3b4..ec72e1a0829 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -30,6 +30,7 @@ export class NullTelemetryServiceShape implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; + readonly devDeviceId = 'someValue.devDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; publicLog() { } @@ -333,6 +334,7 @@ function removePropertiesWithPossibleUserInfo(property: string): string { { label: 'Slack Token', regex: /xox[pbar]\-[A-Za-z0-9]/ }, { label: 'GitHub Token', regex: /(gh[psuro]_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})/ }, { label: 'Generic Secret', regex: /(key|token|sig|secret|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/i }, + { label: 'CLI Credentials', regex: /((login|psexec|(certutil|psexec)\.exe).{1,50}(\s-u(ser(name)?)?\s+.{3,100})?\s-(admin|user|vm|root)?p(ass(word)?)?\s+["']?[^$\-\/\s]|(^|[\s\r\n\\])net(\.exe)?.{1,5}(user\s+|share\s+\/user:| user -? secrets ? set) \s + [^ $\s \/])/ }, { label: 'Email', regex: /@[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+/ } // Regex which matches @*.site ]; diff --git a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts index 6dc9a9fa9d6..95e993f8462 100644 --- a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts +++ b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts @@ -5,8 +5,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey } from 'vs/platform/telemetry/common/telemetry'; -import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { machineIdKey, sqmIdKey, devDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId, resolvedevDeviceId as resolveNodedevDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; export async function resolveMachineId(stateService: IStateService, logService: ILogService): Promise { // Call the node layers implementation to avoid code duplication @@ -20,3 +20,9 @@ export async function resolveSqmId(stateService: IStateService, logService: ILog stateService.setItem(sqmIdKey, sqmId); return sqmId; } + +export async function resolvedevDeviceId(stateService: IStateService, logService: ILogService): Promise { + const devDeviceId = await resolveNodedevDeviceId(stateService, logService); + stateService.setItem(devDeviceIdKey, devDeviceId); + return devDeviceId; +} diff --git a/src/vs/platform/telemetry/node/telemetry.ts b/src/vs/platform/telemetry/node/telemetry.ts index 72f87dddf35..d1770958d3b 100644 --- a/src/vs/platform/telemetry/node/telemetry.ts +++ b/src/vs/platform/telemetry/node/telemetry.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { join } from 'vs/base/common/path'; import { Promises } from 'vs/base/node/pfs'; @@ -21,7 +22,7 @@ export async function buildTelemetryMessage(appRoot: string, extensionsPath?: st const files = await Promises.readdir(extensionsPath); for (const file of files) { try { - const fileStat = await Promises.stat(join(extensionsPath, file)); + const fileStat = await fs.promises.stat(join(extensionsPath, file)); if (fileStat.isDirectory()) { dirs.push(file); } @@ -39,15 +40,15 @@ export async function buildTelemetryMessage(appRoot: string, extensionsPath?: st } for (const folder of telemetryJsonFolders) { - const contents = (await Promises.readFile(join(extensionsPath, folder, 'telemetry.json'))).toString(); + const contents = (await fs.promises.readFile(join(extensionsPath, folder, 'telemetry.json'))).toString(); mergeTelemetry(contents, folder); } } - let contents = (await Promises.readFile(join(appRoot, 'telemetry-core.json'))).toString(); + let contents = (await fs.promises.readFile(join(appRoot, 'telemetry-core.json'))).toString(); mergeTelemetry(contents, 'vscode-core'); - contents = (await Promises.readFile(join(appRoot, 'telemetry-extensions.json'))).toString(); + contents = (await fs.promises.readFile(join(appRoot, 'telemetry-extensions.json'))).toString(); mergeTelemetry(contents, 'vscode-extensions'); return JSON.stringify(mergedTelemetry, null, 4); diff --git a/src/vs/platform/telemetry/node/telemetryUtils.ts b/src/vs/platform/telemetry/node/telemetryUtils.ts index cb5a03fd687..4f8fa8c8a5b 100644 --- a/src/vs/platform/telemetry/node/telemetryUtils.ts +++ b/src/vs/platform/telemetry/node/telemetryUtils.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { isMacintosh } from 'vs/base/common/platform'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateReadService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { machineIdKey, sqmIdKey, devDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; export async function resolveMachineId(stateService: IStateReadService, logService: ILogService): Promise { @@ -29,3 +29,12 @@ export async function resolveSqmId(stateService: IStateReadService, logService: return sqmId; } + +export async function resolvedevDeviceId(stateService: IStateReadService, logService: ILogService): Promise { + let devDeviceId = stateService.getItem(devDeviceIdKey); + if (typeof devDeviceId !== 'string') { + devDeviceId = await getdevDeviceId(logService.error.bind(logService)); + } + + return devDeviceId; +} diff --git a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts index 8c16031dede..33a22c391b2 100644 --- a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts +++ b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { ITelemetryItem, ITelemetryUnloadState } from '@microsoft/1ds-core-js'; -import * as assert from 'assert'; +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OneDataSystemWebAppender } from 'vs/platform/telemetry/browser/1dsAppender'; import { IAppInsightsCore } from 'vs/platform/telemetry/common/1dsAppender'; @@ -28,14 +29,17 @@ suite('AIAdapter', () => { const prefix = 'prefix'; + teardown(() => { + adapter.flush(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + setup(() => { appInsightsMock = new AppInsightsCoreMock(); adapter = new OneDataSystemWebAppender(false, prefix, undefined!, () => appInsightsMock); }); - teardown(() => { - adapter.flush(); - }); test('Simple event', () => { adapter.log('testEvent'); diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 8524e9a6996..bc75667e949 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -2,9 +2,9 @@ * 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 assert from 'assert'; import * as sinon from 'sinon'; -import * as sinonTest from 'sinon-test'; +import sinonTest from 'sinon-test'; import { mainWindow } from 'vs/base/browser/window'; import * as Errors from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts index 8de29c1eeeb..2fe9b7a6477 100644 --- a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts +++ b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { Event } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 8c6575f872e..5cd3d9be955 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -5,6 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import type { IPromptInputModel, ISerializedPromptInputModel } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; import { ITerminalOutputMatch, ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; @@ -161,23 +162,21 @@ export interface IBufferMarkCapability { export interface ICommandDetectionCapability { readonly type: TerminalCapability.CommandDetection; + readonly promptInputModel: IPromptInputModel; readonly commands: readonly ITerminalCommand[]; /** The command currently being executed, otherwise undefined. */ readonly executingCommand: string | undefined; readonly executingCommandObject: ITerminalCommand | undefined; /** The current cwd at the cursor's position. */ readonly cwd: string | undefined; - /** - * Whether a command is currently being input. If the a command is current not being input or - * the state cannot reliably be detected the fallback of undefined will be used. - */ - readonly hasInput: boolean | undefined; readonly currentCommand: ICurrentPartialCommand | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; readonly onCommandExecuted: Event; readonly onCommandInvalidated: Event; readonly onCurrentCommandInvalidated: Event; + setContinuationPrompt(value: string): void; + setPromptTerminator(value: string, lastPromptLine: string): void; setCwd(value: string): void; setIsWindowsPty(value: boolean): void; setIsCommandStorageDisabled(): void; @@ -240,6 +239,7 @@ export interface IPartialCommandDetectionCapability { interface IBaseTerminalCommand { // Mandatory command: string; + commandLineConfidence: 'low' | 'medium' | 'high'; isTrusted: boolean; timestamp: number; duration: number; @@ -262,6 +262,7 @@ export interface ITerminalCommand extends IBaseTerminalCommand { readonly aliases?: string[][]; readonly wasReplayed?: boolean; + extractCommandLine(): string; getOutput(): string | undefined; getOutputMatch(outputMatcher: ITerminalOutputMatcher): ITerminalOutputMatch | undefined; hasOutput(): boolean; @@ -300,6 +301,7 @@ export interface IMarkProperties { export interface ISerializedCommandDetectionCapability { isWindowsPty: boolean; commands: ISerializedTerminalCommand[]; + promptInputModel: ISerializedPromptInputModel | undefined; } export interface IPtyHostProcessReplayEvent { events: ReplayEntry[]; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts new file mode 100644 index 00000000000..9e7c183bb45 --- /dev/null +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts @@ -0,0 +1,487 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import type { ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { throttle } from 'vs/base/common/decorators'; + +// Importing types is safe in any layer +// eslint-disable-next-line local/code-import-patterns +import type { Terminal, IMarker, IBufferCell, IBufferLine, IBuffer } from '@xterm/headless'; + +const enum PromptInputState { + Unknown = 0, + Input = 1, + Execute = 2, +} + +/** + * A model of the prompt input state using shell integration and analyzing the terminal buffer. This + * may not be 100% accurate but provides a best guess. + */ +export interface IPromptInputModel extends IPromptInputModelState { + readonly onDidStartInput: Event; + readonly onDidChangeInput: Event; + readonly onDidFinishInput: Event; + /** + * Fires immediately before {@link onDidFinishInput} when a SIGINT/Ctrl+C/^C is detected. + */ + readonly onDidInterrupt: Event; + + /** + * Gets the prompt input as a user-friendly string where `|` is the cursor position and `[` and + * `]` wrap any ghost text. + */ + getCombinedString(): string; +} + +export interface IPromptInputModelState { + /** + * The full prompt input include ghost text. + */ + readonly value: string; + /** + * The prompt input up to the cursor index, this will always exclude the ghost text. + */ + readonly prefix: string; + /** + * The prompt input from the cursor to the end, this _does not_ include ghost text. + */ + readonly suffix: string; + /** + * The index of the cursor in {@link value}. + */ + readonly cursorIndex: number; + /** + * The index of the start of ghost text in {@link value}. This is -1 when there is no ghost + * text. + */ + readonly ghostTextIndex: number; +} + +export interface ISerializedPromptInputModel { + readonly modelState: IPromptInputModelState; + readonly commandStartX: number; + readonly lastPromptLine: string | undefined; + readonly continuationPrompt: string | undefined; + readonly lastUserInput: string; +} + +export class PromptInputModel extends Disposable implements IPromptInputModel { + private _state: PromptInputState = PromptInputState.Unknown; + + private _commandStartMarker: IMarker | undefined; + private _commandStartX: number = 0; + private _lastPromptLine: string | undefined; + private _continuationPrompt: string | undefined; + + private _lastUserInput: string = ''; + + private _value: string = ''; + get value() { return this._value; } + get prefix() { return this._value.substring(0, this._cursorIndex); } + get suffix() { return this._value.substring(this._cursorIndex, this._ghostTextIndex === -1 ? undefined : this._ghostTextIndex); } + + private _cursorIndex: number = 0; + get cursorIndex() { return this._cursorIndex; } + + private _ghostTextIndex: number = -1; + get ghostTextIndex() { return this._ghostTextIndex; } + + private readonly _onDidStartInput = this._register(new Emitter()); + readonly onDidStartInput = this._onDidStartInput.event; + private readonly _onDidChangeInput = this._register(new Emitter()); + readonly onDidChangeInput = this._onDidChangeInput.event; + private readonly _onDidFinishInput = this._register(new Emitter()); + readonly onDidFinishInput = this._onDidFinishInput.event; + private readonly _onDidInterrupt = this._register(new Emitter()); + readonly onDidInterrupt = this._onDidInterrupt.event; + + constructor( + private readonly _xterm: Terminal, + onCommandStart: Event, + onCommandExecuted: Event, + @ILogService private readonly _logService: ILogService + ) { + super(); + + this._register(Event.any( + this._xterm.onCursorMove, + this._xterm.onData, + this._xterm.onWriteParsed, + )(() => this._sync())); + this._register(this._xterm.onData(e => this._handleUserInput(e))); + + this._register(onCommandStart(e => this._handleCommandStart(e as { marker: IMarker }))); + this._register(onCommandExecuted(() => this._handleCommandExecuted())); + + this._register(this.onDidStartInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidStartInput'))); + this._register(this.onDidChangeInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidChangeInput'))); + this._register(this.onDidFinishInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidFinishInput'))); + this._register(this.onDidInterrupt(() => this._logCombinedStringIfTrace('PromptInputModel#onDidInterrupt'))); + } + + private _logCombinedStringIfTrace(message: string) { + // Only generate the combined string if trace + if (this._logService.getLevel() === LogLevel.Trace) { + this._logService.trace(message, this.getCombinedString()); + } + } + + setContinuationPrompt(value: string): void { + this._continuationPrompt = value; + this._sync(); + } + + setLastPromptLine(value: string): void { + this._lastPromptLine = value; + this._sync(); + } + + setConfidentCommandLine(value: string): void { + if (this._value !== value) { + this._value = value; + this._cursorIndex = -1; + this._ghostTextIndex = -1; + this._onDidChangeInput.fire(this._createStateObject()); + } + } + + getCombinedString(): string { + const value = this._value.replaceAll('\n', '\u23CE'); + if (this._cursorIndex === -1) { + return value; + } + let result = `${value.substring(0, this.cursorIndex)}|`; + if (this.ghostTextIndex !== -1) { + result += `${value.substring(this.cursorIndex, this.ghostTextIndex)}[`; + result += `${value.substring(this.ghostTextIndex)}]`; + } else { + result += value.substring(this.cursorIndex); + } + return result; + } + + serialize(): ISerializedPromptInputModel { + return { + modelState: this._createStateObject(), + commandStartX: this._commandStartX, + lastPromptLine: this._lastPromptLine, + continuationPrompt: this._continuationPrompt, + lastUserInput: this._lastUserInput + }; + } + + deserialize(serialized: ISerializedPromptInputModel): void { + this._value = serialized.modelState.value; + this._cursorIndex = serialized.modelState.cursorIndex; + this._ghostTextIndex = serialized.modelState.ghostTextIndex; + this._commandStartX = serialized.commandStartX; + this._lastPromptLine = serialized.lastPromptLine; + this._continuationPrompt = serialized.continuationPrompt; + this._lastUserInput = serialized.lastUserInput; + } + + private _handleCommandStart(command: { marker: IMarker }) { + if (this._state === PromptInputState.Input) { + return; + } + + this._state = PromptInputState.Input; + this._commandStartMarker = command.marker; + this._commandStartX = this._xterm.buffer.active.cursorX; + this._value = ''; + this._cursorIndex = 0; + this._onDidStartInput.fire(this._createStateObject()); + this._onDidChangeInput.fire(this._createStateObject()); + + // Trigger a sync if prompt terminator is set as that could adjust the command start X + if (this._lastPromptLine) { + if (this._commandStartX !== this._lastPromptLine.length) { + const line = this._xterm.buffer.active.getLine(this._commandStartMarker.line); + if (line?.translateToString(true).startsWith(this._lastPromptLine)) { + this._commandStartX = this._lastPromptLine.length; + this._sync(); + } + } + } + } + + private _handleCommandExecuted() { + if (this._state === PromptInputState.Execute) { + return; + } + + this._cursorIndex = -1; + + // Remove any ghost text from the input if it exists on execute + if (this._ghostTextIndex !== -1) { + this._value = this._value.substring(0, this._ghostTextIndex); + this._ghostTextIndex = -1; + } + + const event = this._createStateObject(); + if (this._lastUserInput === '\u0003') { + this._lastUserInput = ''; + this._onDidInterrupt.fire(event); + } + + this._state = PromptInputState.Execute; + this._onDidFinishInput.fire(event); + this._onDidChangeInput.fire(event); + } + + @throttle(0) + private _sync() { + try { + this._doSync(); + } catch (e) { + this._logService.error('Error while syncing prompt input model', e); + } + } + + private _doSync() { + if (this._state !== PromptInputState.Input) { + return; + } + + const commandStartY = this._commandStartMarker?.line; + if (commandStartY === undefined) { + return; + } + + const buffer = this._xterm.buffer.active; + let line = buffer.getLine(commandStartY); + const commandLine = line?.translateToString(true, this._commandStartX); + if (!line || commandLine === undefined) { + this._logService.trace(`PromptInputModel#_sync: no line`); + return; + } + + const absoluteCursorY = buffer.baseY + buffer.cursorY; + let value = commandLine; + let ghostTextIndex = -1; + let cursorIndex: number; + if (absoluteCursorY === commandStartY) { + cursorIndex = this._getRelativeCursorIndex(this._commandStartX, buffer, line); + } else { + cursorIndex = commandLine.trimEnd().length; + } + + // Detect ghost text by looking for italic or dim text in or after the cursor and + // non-italic/dim text in the cell closest non-whitespace cell before the cursor + if (absoluteCursorY === commandStartY && buffer.cursorX > 1) { + // Ghost text in pwsh only appears to happen on the cursor line + ghostTextIndex = this._scanForGhostText(buffer, line, cursorIndex); + } + + // From command start line to cursor line + for (let y = commandStartY + 1; y <= absoluteCursorY; y++) { + line = buffer.getLine(y); + const lineText = line?.translateToString(true); + if (lineText && line) { + // Check if the line wrapped without a new line (continuation) + if (line.isWrapped) { + value += lineText; + const relativeCursorIndex = this._getRelativeCursorIndex(0, buffer, line); + if (absoluteCursorY === y) { + cursorIndex += relativeCursorIndex; + } else { + cursorIndex += lineText.length; + } + } + // Verify continuation prompt if we have it, if this line doesn't have it then the + // user likely just pressed enter. + else if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { + const trimmedLineText = this._trimContinuationPrompt(lineText); + value += `\n${trimmedLineText}`; + if (absoluteCursorY === y) { + const continuationCellWidth = this._getContinuationPromptCellWidth(line, lineText); + const relativeCursorIndex = this._getRelativeCursorIndex(continuationCellWidth, buffer, line); + cursorIndex += relativeCursorIndex + 1; + } else { + cursorIndex += trimmedLineText.length + 1; + } + } else { + break; + } + } + } + + // Below cursor line + for (let y = absoluteCursorY + 1; y < buffer.baseY + this._xterm.rows; y++) { + line = buffer.getLine(y); + const lineText = line?.translateToString(true); + if (lineText && line) { + if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { + value += `\n${this._trimContinuationPrompt(lineText)}`; + } else { + break; + } + } else { + break; + } + } + + if (this._logService.getLevel() === LogLevel.Trace) { + this._logService.trace(`PromptInputModel#_sync: ${this.getCombinedString()}`); + } + + // Adjust trailing whitespace + { + let trailingWhitespace = this._value.length - this._value.trimEnd().length; + + // Handle backspace key + if (this._lastUserInput === '\x7F') { + this._lastUserInput = ''; + if (cursorIndex === this._cursorIndex - 1) { + // If trailing whitespace is being increased by removing a non-whitespace character + if (this._value.trimEnd().length > value.trimEnd().length && value.trimEnd().length <= cursorIndex) { + trailingWhitespace = Math.max((this._value.length - 1) - value.trimEnd().length, 0); + } + // Standard case; subtract from trailing whitespace + else { + trailingWhitespace = Math.max(trailingWhitespace - 1, 0); + } + + } + } + + // Handle delete key + if (this._lastUserInput === '\x1b[3~') { + this._lastUserInput = ''; + if (cursorIndex === this._cursorIndex) { + trailingWhitespace = Math.max(trailingWhitespace - 1, 0); + } + } + + const valueLines = value.split('\n'); + const isMultiLine = valueLines.length > 1; + const valueEndTrimmed = value.trimEnd(); + if (!isMultiLine) { + // Adjust trimmed whitespace value based on cursor position + if (valueEndTrimmed.length < value.length) { + // Handle space key + if (this._lastUserInput === ' ') { + this._lastUserInput = ''; + if (cursorIndex > valueEndTrimmed.length && cursorIndex > this._cursorIndex) { + trailingWhitespace++; + } + } + trailingWhitespace = Math.max(cursorIndex - valueEndTrimmed.length, trailingWhitespace, 0); + } + + // Handle case where a non-space character is inserted in the middle of trailing whitespace + const charBeforeCursor = cursorIndex === 0 ? '' : value[cursorIndex - 1]; + if (trailingWhitespace > 0 && cursorIndex === this._cursorIndex + 1 && this._lastUserInput !== '' && charBeforeCursor !== ' ') { + trailingWhitespace = this._value.length - this._cursorIndex; + } + } + + if (isMultiLine) { + valueLines[valueLines.length - 1] = valueLines.at(-1)?.trimEnd() ?? ''; + const continuationOffset = (valueLines.length - 1) * (this._continuationPrompt?.length ?? 0); + trailingWhitespace = Math.max(0, cursorIndex - value.length - continuationOffset); + } + + value = valueLines.map(e => e.trimEnd()).join('\n') + ' '.repeat(trailingWhitespace); + } + + if (this._value !== value || this._cursorIndex !== cursorIndex || this._ghostTextIndex !== ghostTextIndex) { + this._value = value; + this._cursorIndex = cursorIndex; + this._ghostTextIndex = ghostTextIndex; + this._onDidChangeInput.fire(this._createStateObject()); + } + } + + private _handleUserInput(e: string) { + this._lastUserInput = e; + } + + /** + * Detect ghost text by looking for italic or dim text in or after the cursor and + * non-italic/dim text in the cell closest non-whitespace cell before the cursor. + */ + private _scanForGhostText(buffer: IBuffer, line: IBufferLine, cursorIndex: number): number { + // Check last non-whitespace character has non-ghost text styles + let ghostTextIndex = -1; + let proceedWithGhostTextCheck = false; + let x = buffer.cursorX; + while (x > 0) { + const cell = line.getCell(--x); + if (!cell) { + break; + } + if (cell.getChars().trim().length > 0) { + proceedWithGhostTextCheck = !this._isCellStyledLikeGhostText(cell); + break; + } + } + + // Check to the end of the line for possible ghost text. For example pwsh's ghost text + // can look like this `Get-|Ch[ildItem]` + if (proceedWithGhostTextCheck) { + let potentialGhostIndexOffset = 0; + let x = buffer.cursorX; + while (x < line.length) { + const cell = line.getCell(x++); + if (!cell || cell.getCode() === 0) { + break; + } + if (this._isCellStyledLikeGhostText(cell)) { + ghostTextIndex = cursorIndex + potentialGhostIndexOffset; + break; + } + potentialGhostIndexOffset += cell.getChars().length; + } + } + + return ghostTextIndex; + } + + private _trimContinuationPrompt(lineText: string): string { + if (this._lineContainsContinuationPrompt(lineText)) { + lineText = lineText.substring(this._continuationPrompt!.length); + } + return lineText; + } + + private _lineContainsContinuationPrompt(lineText: string): boolean { + return !!(this._continuationPrompt && lineText.startsWith(this._continuationPrompt)); + } + + private _getContinuationPromptCellWidth(line: IBufferLine, lineText: string): number { + if (!this._continuationPrompt || !lineText.startsWith(this._continuationPrompt)) { + return 0; + } + let buffer = ''; + let x = 0; + while (buffer !== this._continuationPrompt) { + buffer += line.getCell(x++)!.getChars(); + } + return x; + } + + private _getRelativeCursorIndex(startCellX: number, buffer: IBuffer, line: IBufferLine): number { + return line?.translateToString(true, startCellX, buffer.cursorX).length ?? 0; + } + + private _isCellStyledLikeGhostText(cell: IBufferCell): boolean { + return !!(cell.isItalic() || cell.isDim()); + } + + private _createStateObject(): IPromptInputModelState { + return Object.freeze({ + value: this._value, + prefix: this.prefix, + suffix: this.suffix, + cursorIndex: this._cursorIndex, + ghostTextIndex: this._ghostTextIndex + }); + } +} diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts index 1fffd61bfe0..b0ecb408155 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts @@ -12,6 +12,7 @@ import type { IBuffer, IBufferLine, Terminal } from '@xterm/headless'; export interface ITerminalCommandProperties { command: string; + commandLineConfidence: 'low' | 'medium' | 'high'; isTrusted: boolean; timestamp: number; duration: number; @@ -33,6 +34,7 @@ export interface ITerminalCommandProperties { export class TerminalCommand implements ITerminalCommand { get command() { return this._properties.command; } + get commandLineConfidence() { return this._properties.commandLineConfidence; } get isTrusted() { return this._properties.isTrusted; } get timestamp() { return this._properties.timestamp; } get duration() { return this._properties.duration; } @@ -71,6 +73,7 @@ export class TerminalCommand implements ITerminalCommand { const executedMarker = serialized.executedLine !== undefined ? xterm.registerMarker(serialized.executedLine - (buffer.baseY + buffer.cursorY)) : undefined; const newCommand = new TerminalCommand(xterm, { command: isCommandStorageDisabled ? '' : serialized.command, + commandLineConfidence: serialized.commandLineConfidence ?? 'low', isTrusted: serialized.isTrusted, promptStartMarker, marker, @@ -99,6 +102,7 @@ export class TerminalCommand implements ITerminalCommand { executedLine: this.executedMarker?.line, executedX: this.executedX, command: isCommandStorageDisabled ? '' : this.command, + commandLineConfidence: isCommandStorageDisabled ? 'low' : this.commandLineConfidence, isTrusted: this.isTrusted, cwd: this.cwd, exitCode: this.exitCode, @@ -109,6 +113,10 @@ export class TerminalCommand implements ITerminalCommand { }; } + extractCommandLine(): string { + return extractCommandLine(this._xterm.buffer.active, this._xterm.cols, this.marker, this.startX, this.executedMarker, this.executedX); + } + getOutput(): string | undefined { if (!this.executedMarker || !this.endMarker) { return undefined; @@ -263,7 +271,9 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { currentContinuationMarker?: IMarker; continuations?: { marker: IMarker; end: number }[]; + cwd?: string; command?: string; + commandLineConfidence?: 'low' | 'medium' | 'high'; isTrusted?: boolean; isInvalid?: boolean; @@ -286,6 +296,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { executedLine: undefined, executedX: undefined, command: '', + commandLineConfidence: 'low', isTrusted: true, cwd, exitCode: undefined, @@ -305,6 +316,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { if ((this.command !== undefined && !this.command.startsWith('\\')) || ignoreCommandLine) { return new TerminalCommand(this._xterm, { command: ignoreCommandLine ? '' : (this.command || ''), + commandLineConfidence: ignoreCommandLine ? 'low' : (this.commandLineConfidence || 'low'), isTrusted: !!this.isTrusted, promptStartMarker: this.promptStartMarker, marker: this.commandStartMarker, @@ -336,6 +348,10 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { } } + extractCommandLine(): string { + return extractCommandLine(this._xterm.buffer.active, this._xterm.cols, this.commandStartMarker, this.commandStartX, this.commandExecutedMarker, this.commandExecutedX); + } + getPromptRowCount(): number { return getPromptRowCount(this, this._xterm.buffer.active); } @@ -345,6 +361,27 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { } } +function extractCommandLine( + buffer: IBuffer, + cols: number, + commandStartMarker: IXtermMarker | undefined, + commandStartX: number | undefined, + commandExecutedMarker: IXtermMarker | undefined, + commandExecutedX: number | undefined +): string { + if (!commandStartMarker || !commandExecutedMarker || commandStartX === undefined || commandExecutedX === undefined) { + return ''; + } + let content = ''; + for (let i = commandStartMarker.line; i <= commandExecutedMarker.line; i++) { + const line = buffer.getLine(i); + if (line) { + content += line.translateToString(true, i === commandStartMarker.line ? commandStartX : 0, i === commandExecutedMarker.line ? commandExecutedX : cols); + } + } + return content; +} + function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string { // Cap the maximum number of lines generated to prevent potential performance problems. This is // more of a sanity check as the wrapped line should already be trimmed down at this point. diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 0d4309f98dd..b48cf6306bd 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -10,11 +10,12 @@ import { Disposable, MandatoryMutableDisposable, MutableDisposable } from 'vs/ba import { ILogService } from 'vs/platform/log/common/log'; import { CommandInvalidationReason, ICommandDetectionCapability, ICommandInvalidationRequest, IHandleCommandOptions, ISerializedCommandDetectionCapability, ISerializedTerminalCommand, ITerminalCommand, IXtermMarker, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal'; +import { ICurrentPartialCommand, PartialTerminalCommand, TerminalCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; +import { PromptInputModel, type IPromptInputModel } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; // Importing types is safe in any layer // eslint-disable-next-line local/code-import-patterns import type { IBuffer, IDisposable, IMarker, Terminal } from '@xterm/headless'; -import { ICurrentPartialCommand, PartialTerminalCommand, TerminalCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; interface ITerminalDimensions { cols: number; @@ -24,8 +25,12 @@ interface ITerminalDimensions { export class CommandDetectionCapability extends Disposable implements ICommandDetectionCapability { readonly type = TerminalCapability.CommandDetection; + private readonly _promptInputModel: PromptInputModel; + get promptInputModel(): IPromptInputModel { return this._promptInputModel; } + protected _commands: TerminalCommand[] = []; private _cwd: string | undefined; + private _promptTerminator: string | undefined; private _currentCommand: PartialTerminalCommand = new PartialTerminalCommand(this._terminal); private _commandMarkers: IMarker[] = []; private _dimensions: ITerminalDimensions; @@ -35,7 +40,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe private _commitCommandFinished?: RunOnceScheduler; private _ptyHeuristicsHooks: ICommandDetectionHeuristicsHooks; - private _ptyHeuristics: MandatoryMutableDisposable; + private readonly _ptyHeuristics: MandatoryMutableDisposable; get commands(): readonly TerminalCommand[] { return this._commands; } get executingCommand(): string | undefined { return this._currentCommand.command; } @@ -50,23 +55,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe return this._currentCommand; } get cwd(): string | undefined { return this._cwd; } - private get _isInputting(): boolean { - return !!(this._currentCommand.commandStartMarker && !this._currentCommand.commandExecutedMarker); - } - - get hasInput(): boolean | undefined { - if (!this._isInputting || !this._currentCommand?.commandStartMarker) { - return undefined; - } - if (this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY === this._currentCommand.commandStartMarker?.line) { - const line = this._terminal.buffer.active.getLine(this._terminal.buffer.active.cursorY)?.translateToString(true, this._currentCommand.commandStartX); - if (line === undefined) { - return undefined; - } - return line.length > 0; - } - return true; - } + get promptTerminator(): string | undefined { return this._promptTerminator; } private readonly _onCommandStarted = this._register(new Emitter()); readonly onCommandStarted = this._onCommandStarted.event; @@ -83,10 +72,49 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe constructor( private readonly _terminal: Terminal, - private readonly _logService: ILogService + @ILogService private readonly _logService: ILogService ) { super(); + this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandExecuted, this._logService)); + + // Pull command line from the buffer if it was not set explicitly + this._register(this.onCommandExecuted(command => { + if (command.commandLineConfidence !== 'high') { + // HACK: onCommandExecuted actually fired with PartialTerminalCommand + const typedCommand = (command as ITerminalCommand | PartialTerminalCommand); + command.command = typedCommand.extractCommandLine(); + command.commandLineConfidence = 'low'; + + // ITerminalCommand + if ('getOutput' in typedCommand) { + if ( + // Markers exist + typedCommand.promptStartMarker && typedCommand.marker && typedCommand.executedMarker && + // Single line command + command.command.indexOf('\n') === -1 && + // Start marker is not on the left-most column + typedCommand.startX !== undefined && typedCommand.startX > 0 + ) { + command.commandLineConfidence = 'medium'; + } + } + // PartialTerminalCommand + else { + if ( + // Markers exist + typedCommand.promptStartMarker && typedCommand.commandStartMarker && typedCommand.commandExecutedMarker && + // Single line command + command.command.indexOf('\n') === -1 && + // Start marker is not on the left-most column + typedCommand.commandStartX !== undefined && typedCommand.commandStartX > 0 + ) { + command.commandLineConfidence = 'medium'; + } + } + } + })); + // Set up platform-specific behaviors const that = this; this._ptyHeuristicsHooks = new class implements ICommandDetectionHeuristicsHooks { @@ -121,6 +149,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe @debounce(500) private _handleCursorMove() { + if (this._store.isDisposed) { + return; + } // Early versions of conpty do not have real support for an alt buffer, in addition certain // commands such as tsc watch will write to the top of the normal buffer. The following // checks when the cursor has moved while the normal buffer is empty and if it is above the @@ -156,6 +187,17 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe } } + setContinuationPrompt(value: string): void { + this._promptInputModel.setContinuationPrompt(value); + } + + // TODO: Simplify this, can everything work off the last line? + setPromptTerminator(promptTerminator: string, lastPromptLine: string) { + this._logService.debug('CommandDetectionCapability#setPromptTerminator', promptTerminator); + this._promptTerminator = promptTerminator; + this._promptInputModel.setLastPromptLine(lastPromptLine); + } + setCwd(value: string) { this._cwd = value; } @@ -281,6 +323,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe handleCommandStart(options?: IHandleCommandOptions): void { this._handleCommandStartOptions = options; + this._currentCommand.cwd = this._cwd; // Only update the column if the line has already been set this._currentCommand.commandStartMarker = options?.marker || this._currentCommand.commandStartMarker; if (this._currentCommand.commandStartMarker?.line === this._terminal.buffer.active.cursorY) { @@ -352,7 +395,12 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe setCommandLine(commandLine: string, isTrusted: boolean) { this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine, isTrusted); this._currentCommand.command = commandLine; + this._currentCommand.commandLineConfidence = 'high'; this._currentCommand.isTrusted = isTrusted; + + if (isTrusted) { + this._promptInputModel.setConfidentCommandLine(commandLine); + } } serialize(): ISerializedCommandDetectionCapability { @@ -363,7 +411,8 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe } return { isWindowsPty: this._ptyHeuristics.value instanceof WindowsPtyHeuristics, - commands + commands, + promptInputModel: this._promptInputModel.serialize(), }; } @@ -398,6 +447,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); this._onCommandFinished.fire(newCommand); } + if (serialized.promptInputModel) { + this._promptInputModel.deserialize(serialized.promptInputModel); + } } } @@ -446,7 +498,7 @@ class UnixPtyHeuristics extends Disposable { })); } - async handleCommandStart(options?: IHandleCommandOptions) { + handleCommandStart(options?: IHandleCommandOptions) { this._hooks.commitCommandFinished(); const currentCommand = this._capability.currentCommand; @@ -500,9 +552,9 @@ class UnixPtyHeuristics extends Disposable { } const enum AdjustCommandStartMarkerConstants { - MaxCheckLineCount = 5, + MaxCheckLineCount = 10, Interval = 20, - MaximumPollCount = 50, + MaximumPollCount = 10, } /** @@ -513,7 +565,7 @@ const enum AdjustCommandStartMarkerConstants { */ class WindowsPtyHeuristics extends Disposable { - private _onCursorMoveListener = this._register(new MutableDisposable()); + private readonly _onCursorMoveListener = this._register(new MutableDisposable()); private _tryAdjustCommandStartMarkerScheduler?: RunOnceScheduler; private _tryAdjustCommandStartMarkerScannedLineCount: number = 0; @@ -599,7 +651,7 @@ class WindowsPtyHeuristics extends Disposable { } } - async handleCommandStart() { + handleCommandStart() { this._capability.currentCommand.commandStartX = this._terminal.buffer.active.cursorX; // On Windows track all cursor movements after the command start sequence @@ -683,7 +735,7 @@ class WindowsPtyHeuristics extends Disposable { } if (scannedLineCount < AdjustCommandStartMarkerConstants.MaxCheckLineCount) { this._tryAdjustCommandStartMarkerScannedLineCount = scannedLineCount; - if (this._tryAdjustCommandStartMarkerPollCount < AdjustCommandStartMarkerConstants.MaximumPollCount) { + if (++this._tryAdjustCommandStartMarkerPollCount < AdjustCommandStartMarkerConstants.MaximumPollCount) { this._tryAdjustCommandStartMarkerScheduler?.schedule(); } else { this._flushPendingHandleCommandStartTask(); @@ -869,7 +921,6 @@ class WindowsPtyHeuristics extends Disposable { if (!line) { return; } - // TODO: fine tune prompt regex to accomodate for unique configurations. const lineText = line.translateToString(true); if (!lineText) { return; @@ -896,6 +947,15 @@ class WindowsPtyHeuristics extends Disposable { } } + // Bash Prompt + const bashPrompt = lineText.match(/^(?\$)/)?.groups?.prompt; + if (bashPrompt) { + const adjustedPrompt = this._adjustPrompt(bashPrompt, lineText, '$'); + if (adjustedPrompt) { + return adjustedPrompt; + } + } + // Python Prompt const pythonPrompt = lineText.match(/^(?>>> )/g)?.groups?.prompt; if (pythonPrompt) { @@ -905,6 +965,14 @@ class WindowsPtyHeuristics extends Disposable { }; } + // Dynamic prompt detection + if (this._capability.promptTerminator && lineText.trim().endsWith(this._capability.promptTerminator)) { + const adjustedPrompt = this._adjustPrompt(lineText, lineText, this._capability.promptTerminator); + if (adjustedPrompt) { + return adjustedPrompt; + } + } + // Command Prompt const cmdMatch = lineText.match(/^(?(\(.+\)\s)?(?:[A-Z]:\\.*>))/); return cmdMatch?.groups?.prompt ? { diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 3a27c6d7a0c..a7480bb99b9 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -75,6 +75,7 @@ export const enum TerminalSettingId { TerminalTitle = 'terminal.integrated.tabs.title', TerminalDescription = 'terminal.integrated.tabs.description', RightClickBehavior = 'terminal.integrated.rightClickBehavior', + MiddleClickBehavior = 'terminal.integrated.middleClickBehavior', Cwd = 'terminal.integrated.cwd', ConfirmOnExit = 'terminal.integrated.confirmOnExit', ConfirmOnKill = 'terminal.integrated.confirmOnKill', @@ -89,16 +90,14 @@ export const enum TerminalSettingId { EnvWindows = 'terminal.integrated.env.windows', EnvironmentChangesIndicator = 'terminal.integrated.environmentChangesIndicator', EnvironmentChangesRelaunch = 'terminal.integrated.environmentChangesRelaunch', + ExperimentalWindowsUseConptyDll = 'terminal.integrated.experimental.windowsUseConptyDll', ShowExitAlert = 'terminal.integrated.showExitAlert', SplitCwd = 'terminal.integrated.splitCwd', WindowsEnableConpty = 'terminal.integrated.windowsEnableConpty', WordSeparators = 'terminal.integrated.wordSeparators', EnableFileLinks = 'terminal.integrated.enableFileLinks', + AllowedLinkSchemes = 'terminal.integrated.allowedLinkSchemes', UnicodeVersion = 'terminal.integrated.unicodeVersion', - LocalEchoLatencyThreshold = 'terminal.integrated.localEchoLatencyThreshold', - LocalEchoEnabled = 'terminal.integrated.localEchoEnabled', - LocalEchoExcludePrograms = 'terminal.integrated.localEchoExcludePrograms', - LocalEchoStyle = 'terminal.integrated.localEchoStyle', EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions', PersistentSessionReviveProcess = 'terminal.integrated.persistentSessionReviveProcess', HideOnStartup = 'terminal.integrated.hideOnStartup', @@ -113,17 +112,10 @@ export const enum TerminalSettingId { ShellIntegrationShowWelcome = 'terminal.integrated.shellIntegration.showWelcome', ShellIntegrationDecorationsEnabled = 'terminal.integrated.shellIntegration.decorationsEnabled', ShellIntegrationCommandHistory = 'terminal.integrated.shellIntegration.history', - ShellIntegrationSuggestEnabled = 'terminal.integrated.shellIntegration.suggestEnabled', EnableImages = 'terminal.integrated.enableImages', SmoothScrolling = 'terminal.integrated.smoothScrolling', IgnoreBracketedPasteMode = 'terminal.integrated.ignoreBracketedPasteMode', FocusAfterRun = 'terminal.integrated.focusAfterRun', - AccessibleViewPreserveCursorPosition = 'terminal.integrated.accessibleViewPreserveCursorPosition', - AccessibleViewFocusOnCommandExecution = 'terminal.integrated.accessibleViewFocusOnCommandExecution', - StickyScrollEnabled = 'terminal.integrated.stickyScroll.enabled', - StickyScrollMaxLineCount = 'terminal.integrated.stickyScroll.maxLineCount', - MouseWheelZoom = 'terminal.integrated.mouseWheelZoom', - ExperimentalInlineChat = 'terminal.integrated.experimentalInlineChat', // Debug settings that are hidden from user @@ -670,6 +662,7 @@ export interface ITerminalProcessOptions { nonce: string; }; windowsEnableConpty: boolean; + windowsUseConptyDll: boolean; environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined; workspaceFolder: IWorkspaceFolder | undefined; } diff --git a/src/vs/platform/terminal/common/terminalRecorder.ts b/src/vs/platform/terminal/common/terminalRecorder.ts index d8fcb026948..417527a976f 100644 --- a/src/vs/platform/terminal/common/terminalRecorder.ts +++ b/src/vs/platform/terminal/common/terminalRecorder.ts @@ -7,7 +7,7 @@ import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/capabili import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; const enum Constants { - MaxRecorderDataSize = 1024 * 1024 // 1MB + MaxRecorderDataSize = 10 * 1024 * 1024 // 10MB } interface RecorderEntry { @@ -91,7 +91,8 @@ export class TerminalRecorder { // No command restoration is needed when relaunching terminals commands: { isWindowsPty: false, - commands: [] + commands: [], + promptInputModel: undefined, } }; } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 798fc50f934..d335e2c27cd 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -21,6 +21,7 @@ import { BufferMarkCapability } from 'vs/platform/terminal/common/capabilities/b import type { ITerminalAddon, Terminal } from '@xterm/headless'; import { URI } from 'vs/base/common/uri'; import { sanitizeCwd } from 'vs/platform/terminal/common/terminalEnvironment'; +import { removeAnsiEscapeCodesFromPrompt } from 'vs/base/common/strings'; /** @@ -40,7 +41,7 @@ import { sanitizeCwd } from 'vs/platform/terminal/common/terminalEnvironment'; /** * The identifier for the first numeric parameter (`Ps`) for OSC commands used by shell integration. */ -const enum ShellIntegrationOscPs { +export const enum ShellIntegrationOscPs { /** * Sequences pioneered by FinalTerm. */ @@ -59,38 +60,86 @@ const enum ShellIntegrationOscPs { } /** - * VS Code-specific shell integration sequences. Some of these are based on more common alternatives - * like those pioneered in FinalTerm. The decision to move to entirely custom sequences was to try - * to improve reliability and prevent the possibility of applications confusing the terminal. If - * multiple shell integration scripts run, VS Code will prioritize the VS Code-specific ones. - * - * It's recommended that authors of shell integration scripts use the common sequences (eg. 133) - * when building general purpose scripts and the VS Code-specific (633) when targeting only VS Code - * or when there are no other alternatives. + * Sequences pioneered by FinalTerm. */ -const enum VSCodeOscPt { +const enum FinalTermOscPt { /** * The start of the prompt, this is expected to always appear at the start of a line. - * Based on FinalTerm's `OSC 133 ; A ST`. + * + * Format: `OSC 133 ; A ST` */ PromptStart = 'A', /** * The start of a command, ie. where the user inputs their command. - * Based on FinalTerm's `OSC 133 ; B ST`. + * + * Format: `OSC 133 ; B ST` */ CommandStart = 'B', /** * Sent just before the command output begins. - * Based on FinalTerm's `OSC 133 ; C ST`. + * + * Format: `OSC 133 ; C ST` */ CommandExecuted = 'C', /** * Sent just after a command has finished. The exit code is optional, when not specified it * means no command was run (ie. enter on empty prompt or ctrl+c). - * Based on FinalTerm's `OSC 133 ; D [; ] ST`. + * + * Format: `OSC 133 ; D [; ] ST` + */ + CommandFinished = 'D', +} + +/** + * VS Code-specific shell integration sequences. Some of these are based on more common alternatives + * like those pioneered in {@link FinalTermOscPt FinalTerm}. The decision to move to entirely custom + * sequences was to try to improve reliability and prevent the possibility of applications confusing + * the terminal. If multiple shell integration scripts run, VS Code will prioritize the VS + * Code-specific ones. + * + * It's recommended that authors of shell integration scripts use the common sequences (`133`) + * when building general purpose scripts and the VS Code-specific (`633`) when targeting only VS + * Code or when there are no other alternatives (eg. {@link CommandLine `633 ; E`}). These sequences + * support mix-and-matching. + */ +const enum VSCodeOscPt { + /** + * The start of the prompt, this is expected to always appear at the start of a line. + * + * Format: `OSC 633 ; A ST` + * + * Based on {@link FinalTermOscPt.PromptStart}. + */ + PromptStart = 'A', + + /** + * The start of a command, ie. where the user inputs their command. + * + * Format: `OSC 633 ; B ST` + * + * Based on {@link FinalTermOscPt.CommandStart}. + */ + CommandStart = 'B', + + /** + * Sent just before the command output begins. + * + * Format: `OSC 633 ; C ST` + * + * Based on {@link FinalTermOscPt.CommandExecuted}. + */ + CommandExecuted = 'C', + + /** + * Sent just after a command has finished. The exit code is optional, when not specified it + * means no command was run (ie. enter on empty prompt or ctrl+c). + * + * Format: `OSC 633 ; D [; ] ST` + * + * Based on {@link FinalTermOscPt.CommandFinished}. */ CommandFinished = 'D', @@ -118,7 +167,7 @@ const enum VSCodeOscPt { * An optional nonce can be provided which is may be required by the terminal in order enable * some features. This helps ensure no malicious command injection has occurred. * - * Format: `OSC 633 ; E [; [; ]] ST`. + * Format: `OSC 633 ; E [; [; ]] ST` */ CommandLine = 'E', @@ -151,8 +200,9 @@ const enum VSCodeOscPt { RightPromptEnd = 'I', /** - * Set an arbitrary property: `OSC 633 ; P ; = ST`, only known properties will - * be handled. + * Set the value of an arbitrary property, only known properties will be handled by VS Code. + * + * Format: `OSC 633 ; P ; = ST` * * Known properties: * @@ -160,13 +210,18 @@ const enum VSCodeOscPt { * - `IsWindows` - Indicates whether the terminal is using a Windows backend like winpty or * conpty. This may be used to enable additional heuristics as the positioning of the shell * integration sequences are not guaranteed to be correct. Valid values: `True`, `False`. + * - `ContinuationPrompt` - Reports the continuation prompt that is printed at the start of + * multi-line inputs. * * WARNING: Any other properties may be changed and are not guaranteed to work in the future. */ Property = 'P', /** - * Sets a mark/point-of-interest in the buffer. `OSC 633 ; SetMark [; Id=] [; Hidden]` + * Sets a mark/point-of-interest in the buffer. + * + * Format: `OSC 633 ; SetMark [; Id=] [; Hidden]` + * * `Id` - The identifier of the mark that can be used to reference it * `Hidden` - When set, the mark will be available to reference internally but will not visible * @@ -180,12 +235,16 @@ const enum VSCodeOscPt { */ const enum ITermOscPt { /** - * Sets a mark/point-of-interest in the buffer. `OSC 1337 ; SetMark` + * Sets a mark/point-of-interest in the buffer. + * + * Format: `OSC 1337 ; SetMark` */ SetMark = 'SetMark', /** - * Reports current working directory (CWD). `OSC 1337 ; CurrentDir= ST` + * Reports current working directory (CWD). + * + * Format: `OSC 1337 ; CurrentDir= ST` */ CurrentDir = 'CurrentDir' } @@ -264,17 +323,17 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati // a type flag through the capability calls const [command, ...args] = data.split(';'); switch (command) { - case 'A': + case FinalTermOscPt.PromptStart: this._createOrGetCommandDetection(this._terminal).handlePromptStart(); return true; - case 'B': + case FinalTermOscPt.CommandStart: // Ignore the command line for these sequences as it's unreliable for example in powerlevel10k this._createOrGetCommandDetection(this._terminal).handleCommandStart({ ignoreCommandLine: true }); return true; - case 'C': + case FinalTermOscPt.CommandExecuted: this._createOrGetCommandDetection(this._terminal).handleCommandExecuted(); return true; - case 'D': { + case FinalTermOscPt.CommandFinished: { const exitCode = args.length === 1 ? parseInt(args[0]) : undefined; this._createOrGetCommandDetection(this._terminal).handleCommandFinished(exitCode); return true; @@ -379,6 +438,10 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return true; } switch (key) { + case 'ContinuationPrompt': { + this._updateContinuationPrompt(removeAnsiEscapeCodesFromPrompt(value)); + return true; + } case 'Cwd': { this._updateCwd(value); return true; @@ -387,6 +450,12 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._createOrGetCommandDetection(this._terminal).setIsWindowsPty(value === 'True' ? true : false); return true; } + case 'Prompt': { + // Remove escape sequences from the user's prompt + const sanitizedValue = value.replace(/\x1b\[[0-9;]*m/g, ''); + this._updatePromptTerminator(sanitizedValue); + return true; + } case 'Task': { this._createOrGetBufferMarkDetection(this._terminal); this.capabilities.get(TerminalCapability.CommandDetection)?.setIsCommandStorageDisabled(); @@ -404,6 +473,24 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return false; } + private _updateContinuationPrompt(value: string) { + if (!this._terminal) { + return; + } + this._createOrGetCommandDetection(this._terminal).setContinuationPrompt(value); + } + + private _updatePromptTerminator(prompt: string) { + if (!this._terminal) { + return; + } + const lastPromptLine = prompt.substring(prompt.lastIndexOf('\n') + 1); + const promptTerminator = lastPromptLine.substring(lastPromptLine.lastIndexOf(' ')); + if (promptTerminator) { + this._createOrGetCommandDetection(this._terminal).setPromptTerminator(promptTerminator, lastPromptLine); + } + } + private _updateCwd(value: string) { value = sanitizeCwd(value); this._createOrGetCwdDetection().updateCwd(value); @@ -490,7 +577,8 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati if (!this._terminal || !this.capabilities.has(TerminalCapability.CommandDetection)) { return { isWindowsPty: false, - commands: [] + commands: [], + promptInputModel: undefined, }; } const result = this._createOrGetCommandDetection(this._terminal).serialize(); diff --git a/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts b/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts index 8c74c72b9c9..ebd0331692e 100644 --- a/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts +++ b/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts @@ -38,7 +38,7 @@ export class ElectronPtyHostStarter extends Disposable implements IPtyHostStarte ) { super(); - this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire()); + this._register(this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire())); // Listen for new windows to establish connection directly to pty host validatedIpcMain.on('vscode:createPtyHostMessageChannel', (e, nonce) => this._onWindowConnection(e, nonce)); this._register(toDisposable(() => { diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index f91b87baed4..402dcc7b723 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -108,14 +108,16 @@ export class PtyHostService extends Disposable implements IPtyHostService { this._register(toDisposable(() => this._disposePtyHost())); this._resolveVariablesRequestStore = this._register(new RequestStore(undefined, this._logService)); - this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables); + this._register(this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables)); // Start the pty host when a window requests a connection, if the starter has that capability. if (this._ptyHostStarter.onRequestConnection) { - Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost()); + this._register(Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost())); } - this._ptyHostStarter.onWillShutdown?.(() => this._wasQuitRequested = true); + if (this._ptyHostStarter.onWillShutdown) { + this._register(this._ptyHostStarter.onWillShutdown(() => this._wasQuitRequested = true)); + } } private get _ignoreProcessNames(): string[] { diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index ec8182b3c1d..f7295637003 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -15,7 +15,6 @@ import { RequestStore } from 'vs/platform/terminal/common/requestStore'; import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, IPersistentTerminalProcessLaunchConfig, ICrossVersionSerializedTerminalState, ISerializedTerminalState, ITerminalProcessOptions, IPtyHostLatencyMeasurement } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; -import { Terminal as XtermTerminal } from '@xterm/headless'; import type { ISerializeOptions, SerializeAddon as XtermSerializeAddon } from '@xterm/addon-serialize'; import type { Unicode11Addon as XtermUnicode11Addon } from '@xterm/addon-unicode11'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto } from 'vs/platform/terminal/common/terminalProcess'; @@ -32,6 +31,14 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { join } from 'path'; import { memoize } from 'vs/base/common/decorators'; import * as performance from 'vs/base/common/performance'; +// ESM-comment-begin +import { Terminal as XtermTerminal } from '@xterm/headless'; +// ESM-comment-end +// ESM-uncomment-begin +// import pkg from '@xterm/headless'; +// type XtermTerminal = pkg.Terminal; +// const { Terminal: XtermTerminal } = pkg; +// ESM-uncomment-end export function traceRpc(_target: any, key: string, descriptor: any) { if (typeof descriptor.value !== 'function') { diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index c59f1ddefd9..7349ac1e9db 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -157,10 +157,26 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; if (options.shellIntegration.suggestEnabled) { envMixin['VSCODE_SUGGEST'] = '1'; } return { newArgs, envMixin }; + } else if (shell === 'bash.exe') { + if (!originalArgs || originalArgs.length === 0) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); + } else if (areZshBashLoginArgs(originalArgs)) { + envMixin['VSCODE_SHELL_LOGIN'] = '1'; + addEnvMixinPathPrefix(options, envMixin); + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); + } + if (!newArgs) { + return undefined; + } + newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array + newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; + return { newArgs, envMixin }; } logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); return undefined; @@ -181,6 +197,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } case 'fish': { @@ -206,6 +223,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } case 'zsh': { @@ -310,16 +328,18 @@ shellIntegrationArgs.set(ShellIntegrationExecutable.PwshLogin, ['-l', '-noexit', shellIntegrationArgs.set(ShellIntegrationExecutable.Zsh, ['-i']); shellIntegrationArgs.set(ShellIntegrationExecutable.ZshLogin, ['-il']); shellIntegrationArgs.set(ShellIntegrationExecutable.Bash, ['--init-file', '{0}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh']); -const loginArgs = ['-login', '-l']; +const pwshLoginArgs = ['-login', '-l']; +const shLoginArgs = ['--login', '-l']; +const shInteractiveArgs = ['-i', '--interactive']; const pwshImpliedArgs = ['-nol', '-nologo']; function arePwshLoginArgs(originalArgs: string | string[]): boolean { if (typeof originalArgs === 'string') { - return loginArgs.includes(originalArgs.toLowerCase()); + return pwshLoginArgs.includes(originalArgs.toLowerCase()); } else { - return originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase()) || + return originalArgs.length === 1 && pwshLoginArgs.includes(originalArgs[0].toLowerCase()) || (originalArgs.length === 2 && - (((loginArgs.includes(originalArgs[0].toLowerCase())) || loginArgs.includes(originalArgs[1].toLowerCase()))) + (((pwshLoginArgs.includes(originalArgs[0].toLowerCase())) || pwshLoginArgs.includes(originalArgs[1].toLowerCase()))) && ((pwshImpliedArgs.includes(originalArgs[0].toLowerCase())) || pwshImpliedArgs.includes(originalArgs[1].toLowerCase()))); } } @@ -333,6 +353,9 @@ function arePwshImpliedArgs(originalArgs: string | string[]): boolean { } function areZshBashLoginArgs(originalArgs: string | string[]): boolean { - return originalArgs === 'string' && loginArgs.includes(originalArgs.toLowerCase()) - || typeof originalArgs !== 'string' && originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase()); + if (typeof originalArgs !== 'string') { + originalArgs = originalArgs.filter(arg => !shInteractiveArgs.includes(arg.toLowerCase())); + } + return originalArgs === 'string' && shLoginArgs.includes(originalArgs.toLowerCase()) + || typeof originalArgs !== 'string' && originalArgs.length === 1 && shLoginArgs.includes(originalArgs[0].toLowerCase()); } diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 79be9013c81..e08cd21dc7b 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { exec } from 'child_process'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -10,7 +11,6 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import * as path from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -102,7 +102,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _processStartupComplete: Promise | undefined; private _windowsShellHelper: WindowsShellHelper | undefined; private _childProcessMonitor: ChildProcessMonitor | undefined; - private _titleInterval: NodeJS.Timer | null = null; + private _titleInterval: NodeJS.Timeout | null = null; private _writeQueue: IWriteObject[] = []; private _writeTimeout: NodeJS.Timeout | undefined; private _delayedResizer: DelayedResizer | undefined; @@ -153,6 +153,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._properties[ProcessPropertyType.InitialCwd] = this._initialCwd; this._properties[ProcessPropertyType.Cwd] = this._initialCwd; const useConpty = this._options.windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309; + const useConptyDll = useConpty && this._options.windowsUseConptyDll; this._ptyOptions = { name, cwd, @@ -161,6 +162,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess cols, rows, useConpty, + useConptyDll, // This option will force conpty to not redraw the whole viewport on launch conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText }; @@ -211,9 +213,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } if (injection.filesToCopy) { for (const f of injection.filesToCopy) { - await Promises.mkdir(path.dirname(f.dest), { recursive: true }); try { - await Promises.copyFile(f.source, f.dest); + await fs.promises.mkdir(path.dirname(f.dest), { recursive: true }); + await fs.promises.copyFile(f.source, f.dest); } catch { // Swallow error, this should only happen when multiple users are on the same // machine. Since the shell integration scripts rarely change, plus the other user @@ -241,7 +243,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private async _validateCwd(): Promise { try { - const result = await Promises.stat(this._initialCwd); + const result = await fs.promises.stat(this._initialCwd); if (!result.isDirectory()) { return { message: localize('launchFail.cwdNotDirectory', "Starting directory (cwd) \"{0}\" is not a directory", this._initialCwd.toString()) }; } @@ -268,7 +270,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } try { - const result = await Promises.stat(executable); + const result = await fs.promises.stat(executable); if (!result.isFile() && !result.isSymbolicLink()) { return { message: localize('launchFail.executableIsNotFileOrSymlink', "Path to shell executable \"{0}\" is not a file or a symlink", slc.executable) }; } @@ -401,7 +403,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess if (this._store.isDisposed) { return; } - this._currentTitle = ptyProcess.process; + // HACK: The node-pty API can return undefined somehow https://github.com/microsoft/vscode/issues/222323 + this._currentTitle = (ptyProcess.process ?? ''); this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: this._currentTitle }); // If fig is installed it may change the title of the process const sanitizedTitle = this.currentTitle.replace(/ \(figterm\)$/g, ''); @@ -608,7 +611,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } this._logService.trace('node-pty.IPty#pid'); try { - return await Promises.readlink(`/proc/${this._ptyProcess.pid}/cwd`); + return await fs.promises.readlink(`/proc/${this._ptyProcess.pid}/cwd`); } catch (error) { return this._initialCwd; } diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts index e289fbc34fc..97a85375c95 100644 --- a/src/vs/platform/terminal/node/terminalProfiles.ts +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as cp from 'child_process'; import { Codicon } from 'vs/base/common/codicons'; import { basename, delimiter, normalize } from 'vs/base/common/path'; @@ -38,7 +39,7 @@ export function detectAvailableProfiles( ): Promise { fsProvider = fsProvider || { existsFile: pfs.SymlinkSupport.existsFile, - readFile: pfs.Promises.readFile + readFile: fs.promises.readFile }; if (isWindows) { return detectAvailableWindowsProfiles( diff --git a/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts new file mode 100644 index 00000000000..667442dd8c9 --- /dev/null +++ b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts @@ -0,0 +1,634 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/* eslint-disable local/code-import-patterns */ + +import type { Terminal } from '@xterm/xterm'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { PromptInputModel, type IPromptInputModelState } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; +import { Emitter } from 'vs/base/common/event'; +import type { ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { notDeepStrictEqual, strictEqual } from 'assert'; +import { timeout } from 'vs/base/common/async'; +import { importAMDNodeModule } from 'vs/amdX'; + +suite('PromptInputModel', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let promptInputModel: PromptInputModel; + let xterm: Terminal; + let onCommandStart: Emitter; + let onCommandExecuted: Emitter; + + async function writePromise(data: string) { + await new Promise(r => xterm.write(data, r)); + } + + function fireCommandStart() { + onCommandStart.fire({ marker: xterm.registerMarker() } as ITerminalCommand); + } + + function fireCommandExecuted() { + onCommandExecuted.fire(null!); + } + + function setContinuationPrompt(prompt: string) { + promptInputModel.setContinuationPrompt(prompt); + } + + async function assertPromptInput(valueWithCursor: string) { + await timeout(0); + + if (promptInputModel.cursorIndex !== -1 && !valueWithCursor.includes('|')) { + throw new Error('assertPromptInput must contain | character'); + } + + const actualValueWithCursor = promptInputModel.getCombinedString(); + strictEqual( + actualValueWithCursor, + valueWithCursor.replaceAll('\n', '\u23CE') + ); + + // This is required to ensure the cursor index is correctly resolved for non-ascii characters + const value = valueWithCursor.replace(/[\|\[\]]/g, ''); + const cursorIndex = valueWithCursor.indexOf('|'); + strictEqual(promptInputModel.value, value); + strictEqual(promptInputModel.cursorIndex, cursorIndex, `value=${promptInputModel.value}`); + } + + setup(async () => { + const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; + xterm = store.add(new TerminalCtor({ allowProposedApi: true })); + onCommandStart = store.add(new Emitter()); + onCommandExecuted = store.add(new Emitter()); + promptInputModel = store.add(new PromptInputModel(xterm, onCommandStart.event, onCommandExecuted.event, new NullLogService)); + }); + + test('basic input and execute', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('foo bar'); + await assertPromptInput('foo bar|'); + + await writePromise('\r\n'); + fireCommandExecuted(); + await assertPromptInput('foo bar'); + + await writePromise('(command output)\r\n$ '); + fireCommandStart(); + await assertPromptInput('|'); + }); + + test('should not fire onDidChangeInput events when nothing changes', async () => { + const events: IPromptInputModelState[] = []; + store.add(promptInputModel.onDidChangeInput(e => events.push(e))); + + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('foo'); + await assertPromptInput('foo|'); + + await writePromise(' bar'); + await assertPromptInput('foo bar|'); + + await writePromise('\r\n'); + fireCommandExecuted(); + await assertPromptInput('foo bar'); + + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('foo bar'); + await assertPromptInput('foo bar|'); + + for (let i = 0; i < events.length - 1; i++) { + notDeepStrictEqual(events[i], events[i + 1], 'not adjacent events should fire with the same value'); + } + }); + + test('should fire onDidInterrupt followed by onDidFinish when ctrl+c is pressed', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('foo'); + await assertPromptInput('foo|'); + + await new Promise(r => { + store.add(promptInputModel.onDidInterrupt(() => { + // Fire onDidFinishInput immediately after onDidInterrupt + store.add(promptInputModel.onDidFinishInput(() => { + r(); + })); + })); + xterm.input('\x03'); + writePromise('^C').then(() => fireCommandExecuted()); + }); + }); + + test('cursor navigation', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('foo bar'); + await assertPromptInput('foo bar|'); + + await writePromise('\x1b[3D'); + await assertPromptInput('foo |bar'); + + await writePromise('\x1b[4D'); + await assertPromptInput('|foo bar'); + + await writePromise('\x1b[3C'); + await assertPromptInput('foo| bar'); + + await writePromise('\x1b[4C'); + await assertPromptInput('foo bar|'); + + await writePromise('\x1b[D'); + await assertPromptInput('foo ba|r'); + + await writePromise('\x1b[C'); + await assertPromptInput('foo bar|'); + }); + + test('ghost text', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('foo\x1b[2m bar\x1b[0m\x1b[4D'); + await assertPromptInput('foo|[ bar]'); + + await writePromise('\x1b[2D'); + await assertPromptInput('f|oo[ bar]'); + }); + + test('wide input (Korean)', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('안영'); + await assertPromptInput('안영|'); + + await writePromise('\r\n컴퓨터'); + await assertPromptInput('안영\n컴퓨터|'); + + await writePromise('\r\n사람'); + await assertPromptInput('안영\n컴퓨터\n사람|'); + + await writePromise('\x1b[G'); + await assertPromptInput('안영\n컴퓨터\n|사람'); + + await writePromise('\x1b[A'); + await assertPromptInput('안영\n|컴퓨터\n사람'); + + await writePromise('\x1b[4C'); + await assertPromptInput('안영\n컴퓨|터\n사람'); + + await writePromise('\x1b[1;4H'); + await assertPromptInput('안|영\n컴퓨터\n사람'); + + await writePromise('\x1b[D'); + await assertPromptInput('|안영\n컴퓨터\n사람'); + }); + + test('emoji input', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('✌️👍'); + await assertPromptInput('✌️👍|'); + + await writePromise('\r\n😎😕😅'); + await assertPromptInput('✌️👍\n😎😕😅|'); + + await writePromise('\r\n🤔🤷😩'); + await assertPromptInput('✌️👍\n😎😕😅\n🤔🤷😩|'); + + await writePromise('\x1b[G'); + await assertPromptInput('✌️👍\n😎😕😅\n|🤔🤷😩'); + + await writePromise('\x1b[A'); + await assertPromptInput('✌️👍\n|😎😕😅\n🤔🤷😩'); + + await writePromise('\x1b[2C'); + await assertPromptInput('✌️👍\n😎😕|😅\n🤔🤷😩'); + + await writePromise('\x1b[1;4H'); + await assertPromptInput('✌️|👍\n😎😕😅\n🤔🤷😩'); + + await writePromise('\x1b[D'); + await assertPromptInput('|✌️👍\n😎😕😅\n🤔🤷😩'); + }); + + suite('trailing whitespace', () => { + test('delete whitespace with backspace', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise(' '); + await assertPromptInput(` |`); + + xterm.input('\x7F', true); // Backspace + await writePromise('\x1b[D'); + await assertPromptInput('|'); + + xterm.input(' '.repeat(4), true); + await writePromise(' '.repeat(4)); + await assertPromptInput(` |`); + + xterm.input('\x1b[D'.repeat(2), true); // Left + await writePromise('\x1b[2D'); + await assertPromptInput(` | `); + + xterm.input('\x7F', true); // Backspace + await writePromise('\x1b[D'); + await assertPromptInput(` | `); + + xterm.input('\x7F', true); // Backspace + await writePromise('\x1b[D'); + await assertPromptInput(`| `); + + xterm.input(' ', true); + await writePromise(' '); + await assertPromptInput(` | `); + + xterm.input(' ', true); + await writePromise(' '); + await assertPromptInput(` | `); + + xterm.input('\x1b[C', true); // Right + await writePromise('\x1b[C'); + await assertPromptInput(` | `); + + xterm.input('a', true); + await writePromise('a'); + await assertPromptInput(` a| `); + + xterm.input('\x7F', true); // Backspace + await writePromise('\x1b[D\x1b[K'); + await assertPromptInput(` | `); + + xterm.input('\x1b[D'.repeat(2), true); // Left + await writePromise('\x1b[2D'); + await assertPromptInput(` | `); + + xterm.input('\x1b[3~', true); // Delete + await writePromise(''); + await assertPromptInput(` | `); + }); + + // TODO: This doesn't work correctly but it doesn't matter too much as it only happens when + // there is a lot of whitespace at the end of a prompt input + test.skip('track whitespace when ConPTY deletes whitespace unexpectedly', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + xterm.input('ls', true); + await writePromise('ls'); + await assertPromptInput(`ls|`); + + xterm.input(' '.repeat(4), true); + await writePromise(' '.repeat(4)); + await assertPromptInput(`ls |`); + + xterm.input(' ', true); + await writePromise('\x1b[4D\x1b[5X\x1b[5C'); // Cursor left x(N-1), delete xN, cursor right xN + await assertPromptInput(`ls |`); + }); + + test('track whitespace beyond cursor', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise(' '.repeat(8)); + await assertPromptInput(`${' '.repeat(8)}|`); + + await writePromise('\x1b[4D'); + await assertPromptInput(`${' '.repeat(4)}|${' '.repeat(4)}`); + }); + }); + + suite('multi-line', () => { + test('basic 2 line', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('echo "a'); + await assertPromptInput(`echo "a|`); + + await writePromise('\n\r\∙ '); + setContinuationPrompt('∙ '); + await assertPromptInput(`echo "a\n|`); + + await writePromise('b'); + await assertPromptInput(`echo "a\nb|`); + }); + + test('basic 3 line', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('echo "a'); + await assertPromptInput(`echo "a|`); + + await writePromise('\n\r\∙ '); + setContinuationPrompt('∙ '); + await assertPromptInput(`echo "a\n|`); + + await writePromise('b'); + await assertPromptInput(`echo "a\nb|`); + + await writePromise('\n\r\∙ '); + setContinuationPrompt('∙ '); + await assertPromptInput(`echo "a\nb\n|`); + + await writePromise('c'); + await assertPromptInput(`echo "a\nb\nc|`); + }); + + test('navigate left in multi-line', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('echo "a'); + await assertPromptInput(`echo "a|`); + + await writePromise('\n\r\∙ '); + setContinuationPrompt('∙ '); + await assertPromptInput(`echo "a\n|`); + + await writePromise('b'); + await assertPromptInput(`echo "a\nb|`); + + await writePromise('\x1b[D'); + await assertPromptInput(`echo "a\n|b`); + + await writePromise('\x1b[@c'); + await assertPromptInput(`echo "a\nc|b`); + + await writePromise('\x1b[K\n\r\∙ '); + await assertPromptInput(`echo "a\nc\n|`); + + await writePromise('b'); + await assertPromptInput(`echo "a\nc\nb|`); + + await writePromise(' foo'); + await assertPromptInput(`echo "a\nc\nb foo|`); + + await writePromise('\x1b[3D'); + await assertPromptInput(`echo "a\nc\nb |foo`); + }); + + test('navigate up in multi-line', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('echo "foo'); + await assertPromptInput(`echo "foo|`); + + await writePromise('\n\r\∙ '); + setContinuationPrompt('∙ '); + await assertPromptInput(`echo "foo\n|`); + + await writePromise('bar'); + await assertPromptInput(`echo "foo\nbar|`); + + await writePromise('\n\r\∙ '); + setContinuationPrompt('∙ '); + await assertPromptInput(`echo "foo\nbar\n|`); + + await writePromise('baz'); + await assertPromptInput(`echo "foo\nbar\nbaz|`); + + await writePromise('\x1b[A'); + await assertPromptInput(`echo "foo\nbar|\nbaz`); + + await writePromise('\x1b[D'); + await assertPromptInput(`echo "foo\nba|r\nbaz`); + + await writePromise('\x1b[D'); + await assertPromptInput(`echo "foo\nb|ar\nbaz`); + + await writePromise('\x1b[D'); + await assertPromptInput(`echo "foo\n|bar\nbaz`); + + await writePromise('\x1b[1;9H'); + await assertPromptInput(`echo "|foo\nbar\nbaz`); + + await writePromise('\x1b[C'); + await assertPromptInput(`echo "f|oo\nbar\nbaz`); + + await writePromise('\x1b[C'); + await assertPromptInput(`echo "fo|o\nbar\nbaz`); + + await writePromise('\x1b[C'); + await assertPromptInput(`echo "foo|\nbar\nbaz`); + }); + + test('navigating up when first line contains invalid/stale trailing whitespace', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('echo "foo \x1b[6D'); + await assertPromptInput(`echo "foo|`); + + await writePromise('\n\r\∙ '); + setContinuationPrompt('∙ '); + await assertPromptInput(`echo "foo\n|`); + + await writePromise('bar'); + await assertPromptInput(`echo "foo\nbar|`); + + await writePromise('\x1b[D'); + await assertPromptInput(`echo "foo\nba|r`); + + await writePromise('\x1b[D'); + await assertPromptInput(`echo "foo\nb|ar`); + + await writePromise('\x1b[D'); + await assertPromptInput(`echo "foo\n|bar`); + }); + }); + + suite('wrapped line (non-continuation)', () => { + test('basic wrapped line', async () => { + xterm.resize(5, 10); + + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('ech'); + await assertPromptInput(`ech|`); + + await writePromise('o '); + await assertPromptInput(`echo |`); + + await writePromise('"a"'); + // HACK: Trailing whitespace is due to flaky detection in wrapped lines (but it doesn't matter much) + await assertPromptInput(`echo "a"| `); + }); + }); + + // To "record a session" for these tests: + // - Enable debug logging + // - Open and clear Terminal output channel + // - Open terminal and perform the test + // - Extract all "parsing data" lines from the terminal + suite('recorded sessions', () => { + async function replayEvents(events: string[]) { + for (const data of events) { + await writePromise(data); + } + } + + suite('Windows 11 (10.0.22621.3447), pwsh 7.4.2, starship prompt 1.10.2', () => { + test('input with ignored ghost text', async () => { + await replayEvents([ + '[?25l]0;C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.4.2.0_x64__8wekyb3d8bbwe\\pwsh.exe[?25h', + '[?25l\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n[?25h', + ']633;P;IsWindows=True', + ']633;P;ContinuationPrompt=\x1b[38\x3b5\x3b8m∙\x1b[0m ', + ']633;A]633;P;Cwd=C:\x5cGithub\x5cmicrosoft\x5cvscode]633;B', + '\r\n03:13:47  vscode   tyriar/prompt_input_model  $⇡  via  v18.18.2 \r\n❯ ', + ]); + fireCommandStart(); + await assertPromptInput('|'); + + await replayEvents([ + '[?25lfakecommand[?25h', + '', + 'fo', + '', + '[?25lfoo[?25h', + '', + ]); + await assertPromptInput('foo|'); + }); + test('input with accepted and run ghost text', async () => { + await replayEvents([ + '[?25l]0;C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.4.2.0_x64__8wekyb3d8bbwe\\pwsh.exe[?25h', + '[?25l\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n[?25h', + ']633;P;IsWindows=True', + ']633;P;ContinuationPrompt=\x1b[38\x3b5\x3b8m∙\x1b[0m ', + ']633;A]633;P;Cwd=C:\x5cGithub\x5cmicrosoft\x5cvscode]633;B', + '\r\n03:41:36  vscode   tyriar/prompt_input_model  $  via  v18.18.2 \r\n❯ ', + ]); + promptInputModel.setContinuationPrompt('∙ '); + fireCommandStart(); + await assertPromptInput('|'); + + await replayEvents([ + '[?25lecho "hello world"[?25h', + '', + ]); + await assertPromptInput('e|[cho "hello world"]'); + + await replayEvents([ + '[?25lecho "hello world"[?25h', + '', + ]); + await assertPromptInput('ec|[ho "hello world"]'); + + await replayEvents([ + '[?25lecho "hello world"[?25h', + '', + ]); + await assertPromptInput('ech|[o "hello world"]'); + + await replayEvents([ + '[?25lecho "hello world"[?25h', + '', + ]); + await assertPromptInput('echo|[ "hello world"]'); + + await replayEvents([ + '[?25lecho "hello world"[?25h', + '', + ]); + await assertPromptInput('echo |["hello world"]'); + + await replayEvents([ + '[?25lecho "hello world"[?25h', + '', + ]); + await assertPromptInput('echo "hello world"|'); + + await replayEvents([ + ']633;E;echo "hello world";ff464d39-bc80-4bae-9ead-b1cafc4adf6f]633;C', + ]); + fireCommandExecuted(); + await assertPromptInput('echo "hello world"'); + + await replayEvents([ + '\r\n', + 'hello world\r\n', + ]); + await assertPromptInput('echo "hello world"'); + + await replayEvents([ + ']633;D;0]633;A]633;P;Cwd=C:\x5cGithub\x5cmicrosoft\x5cvscode]633;B', + '\r\n03:41:42  vscode   tyriar/prompt_input_model  $  via  v18.18.2 \r\n❯ ', + ]); + fireCommandStart(); + await assertPromptInput('|'); + }); + + test('input, go to start (ctrl+home), delete word in front (ctrl+delete)', async () => { + await replayEvents([ + '[?25l]0;C:\Program Files\WindowsApps\Microsoft.PowerShell_7.4.2.0_x64__8wekyb3d8bbwe\pwsh.exe[?25h', + '[?25l\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n[?25h', + ']633;P;IsWindows=True', + ']633;P;ContinuationPrompt=\x1b[38\x3b5\x3b8m∙\x1b[0m ', + ']633;A]633;P;Cwd=C:\x5cGithub\x5cmicrosoft\x5cvscode]633;B', + '\r\n16:07:06  vscode   tyriar/210662  $!  via  v18.18.2 \r\n❯ ', + ]); + fireCommandStart(); + await assertPromptInput('|'); + + await replayEvents([ + '[?25lGit push[?25h', + '', + '[?25lGet-ChildItem -Path a[?25h', + '', + '[?25lGet-ChildItem -Path a[?25h', + ]); + await assertPromptInput('Get|[-ChildItem -Path a]'); + + await replayEvents([ + '', + '[?25l[?25h', + '', + ]); + + // Don't force a sync, the prompt input model should update by itself + await timeout(0); + const actualValueWithCursor = promptInputModel.getCombinedString(); + strictEqual( + actualValueWithCursor, + '|'.replaceAll('\n', '\u23CE') + ); + }); + }); + }); +}); diff --git a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts index b1c523a2228..66b317c468b 100644 --- a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts +++ b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 2f68a7be2df..06f8aeed6a6 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -12,9 +12,9 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { getShellIntegrationInjection, getWindowsBuildNumber, IShellIntegrationConfigInjection } from 'vs/platform/terminal/node/terminalEnvironment'; -const enabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, environmentVariableCollections: undefined, workspaceFolder: undefined }; -const disabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, environmentVariableCollections: undefined, workspaceFolder: undefined }; -const winptyProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const enabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const disabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' }, windowsEnableConpty: true, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const winptyProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false, nonce: '' }, windowsEnableConpty: false, windowsUseConptyDll: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; const pwshExe = process.platform === 'win32' ? 'pwsh.exe' : 'pwsh'; const repoRoot = process.platform === 'win32' ? process.cwd()[0].toLowerCase() + process.cwd().substring(1) : process.cwd(); const logService = new NullLogService(); @@ -186,7 +186,8 @@ suite('platform - terminalEnvironment', () => { `${repoRoot}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh` ], envMixin: { - VSCODE_INJECTION: '1' + VSCODE_INJECTION: '1', + VSCODE_STABLE: '0' } }); deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService), enabledExpectedResult); @@ -201,7 +202,8 @@ suite('platform - terminalEnvironment', () => { ], envMixin: { VSCODE_INJECTION: '1', - VSCODE_SHELL_LOGIN: '1' + VSCODE_SHELL_LOGIN: '1', + VSCODE_STABLE: '0' } }); test('when array', () => { diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index a455c0ec614..68d93cbc233 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IButtonStyles } from 'vs/base/browser/ui/button/button'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground } from 'vs/platform/theme/common/colorRegistry'; +import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder } from 'vs/platform/theme/common/colorRegistry'; import { IProgressBarStyles } from 'vs/base/browser/ui/progressbar/progressbar'; import { ICheckboxStyles, IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { IDialogStyles } from 'vs/base/browser/ui/dialog/dialog'; @@ -16,13 +16,14 @@ import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ISelectBoxStyles } from 'vs/base/browser/ui/selectBox/selectBox'; import { Color } from 'vs/base/common/color'; import { IMenuStyles } from 'vs/base/browser/ui/menu/menu'; +import { IRadioStyles } from 'vs/base/browser/ui/radio/radio'; export type IStyleOverride = { [P in keyof T]?: ColorIdentifier | undefined; }; -function overrideStyles(override: IStyleOverride, styles: T): any { - const result = { ...styles } as { [P in keyof T]: string | undefined }; +function overrideStyles(override: IStyleOverride, styles: T): any { + const result: { [P in keyof T]: string | undefined } = { ...styles }; for (const key in override) { const val = override[key]; result[key] = val !== undefined ? asCssVariable(val) : undefined; @@ -41,6 +42,7 @@ export const defaultKeybindingLabelStyles: IKeybindingLabelStyles = { export function getKeybindingLabelStyles(override: IStyleOverride): IKeybindingLabelStyles { return overrideStyles(override, defaultKeybindingLabelStyles); } + export const defaultButtonStyles: IButtonStyles = { buttonForeground: asCssVariable(buttonForeground), buttonSeparator: asCssVariable(buttonSeparator), @@ -70,6 +72,16 @@ export const defaultToggleStyles: IToggleStyles = { inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground) }; +export const defaultRadioStyles: IRadioStyles = { + activeForeground: asCssVariable(radioActiveForeground), + activeBackground: asCssVariable(radioActiveBackground), + activeBorder: asCssVariable(radioActiveBorder), + inactiveForeground: asCssVariable(radioInactiveForeground), + inactiveBackground: asCssVariable(radioInactiveBackground), + inactiveBorder: asCssVariable(radioInactiveBorder), + inactiveHoverBackground: asCssVariable(radioInactiveHoverBackground), +}; + export function getToggleStyles(override: IStyleOverride): IToggleStyles { return overrideStyles(override, defaultToggleStyles); } @@ -174,6 +186,9 @@ export const defaultListStyles: IListStyles = { listHoverOutline: asCssVariable(activeContrastBorder), treeIndentGuidesStroke: asCssVariable(treeIndentGuidesStroke), treeInactiveIndentGuidesStroke: asCssVariable(treeInactiveIndentGuidesStroke), + treeStickyScrollBackground: undefined, + treeStickyScrollBorder: undefined, + treeStickyScrollShadow: asCssVariable(scrollbarShadow), tableColumnsBorder: asCssVariable(tableColumnsBorder), tableOddRowsBackgroundColor: asCssVariable(tableOddRowsBackgroundColor), }; @@ -216,6 +231,9 @@ export const defaultSelectBoxStyles: ISelectBoxStyles = { tableOddRowsBackgroundColor: undefined, treeIndentGuidesStroke: undefined, treeInactiveIndentGuidesStroke: undefined, + treeStickyScrollBackground: undefined, + treeStickyScrollBorder: undefined, + treeStickyScrollShadow: undefined }; export function getSelectBoxStyles(override: IStyleOverride): ISelectBoxStyles { diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts index 6bfce5b466d..96ec62103aa 100644 --- a/src/vs/platform/theme/browser/iconsStyleSheet.ts +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -31,27 +31,31 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc getCSS() { const productIconTheme = themeService ? themeService.getProductIconTheme() : new UnthemedProductIconTheme(); const usedFontIds: { [id: string]: IconFontDefinition } = {}; - const formatIconRule = (contribution: IconContribution): string | undefined => { + + const rules: string[] = []; + const rootAttribs: string[] = []; + for (const contribution of iconRegistry.getIcons()) { const definition = productIconTheme.getIcon(contribution); if (!definition) { - return undefined; + continue; } + const fontContribution = definition.font; + const fontFamilyVar = `--vscode-icon-${contribution.id}-font-family`; + const contentVar = `--vscode-icon-${contribution.id}-content`; if (fontContribution) { usedFontIds[fontContribution.id] = fontContribution.definition; - return `.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; font-family: ${asCSSPropertyValue(fontContribution.id)}; }`; - } - // default font (codicon) - return `.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; }`; - }; - - const rules = []; - for (const contribution of iconRegistry.getIcons()) { - const rule = formatIconRule(contribution); - if (rule) { - rules.push(rule); + rootAttribs.push( + `${fontFamilyVar}: ${asCSSPropertyValue(fontContribution.id)};`, + `${contentVar}: '${definition.fontCharacter}';`, + ); + rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; font-family: ${asCSSPropertyValue(fontContribution.id)}; }`); + } else { + rootAttribs.push(`${contentVar}: '${definition.fontCharacter}'; ${fontFamilyVar}: 'codicon';`); + rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; }`); } } + for (const id in usedFontIds) { const definition = usedFontIds[id]; const fontWeight = definition.weight ? `font-weight: ${definition.weight};` : ''; @@ -59,6 +63,9 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc const src = definition.src.map(l => `${asCSSUrl(l.location)} format('${l.format}')`).join(', '); rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)};${fontWeight}${fontStyle} font-display: block; }`); } + + rules.push(`:root { ${rootAttribs.join(' ')} }`); + return rules.join('\n'); } }; diff --git a/src/vs/platform/theme/common/colorUtils.ts b/src/vs/platform/theme/common/colorUtils.ts index 2388e7cb702..14ceea8847b 100644 --- a/src/vs/platform/theme/common/colorUtils.ts +++ b/src/vs/platform/theme/common/colorUtils.ts @@ -7,10 +7,11 @@ import { assertNever } from 'vs/base/common/assert'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import * as platform from 'vs/platform/registry/common/platform'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; +import * as nls from 'vs/nls'; // ------ API types @@ -19,7 +20,7 @@ export type ColorIdentifier = string; export interface ColorContribution { readonly id: ColorIdentifier; readonly description: string; - readonly defaults: ColorDefaults | null; + readonly defaults: ColorDefaults | ColorValue | null; readonly needsTransparency: boolean; readonly deprecationMessage: string | undefined; } @@ -68,6 +69,9 @@ export interface ColorDefaults { hcLight: ColorValue | null; } +export function isColorDefaults(value: unknown): value is ColorDefaults { + return value !== null && typeof value === 'object' && 'light' in value && 'dark' in value; +} /** * A Color Value is either a color literal, a reference to an other color or a derived color @@ -79,6 +83,8 @@ export const Extensions = { ColorContribution: 'base.contributions.colors' }; +export const DEFAULT_COLOR_CONFIG_VALUE = 'default'; + export interface IColorRegistry { readonly onDidChangeSchema: Event; @@ -117,33 +123,56 @@ export interface IColorRegistry { */ getColorReferenceSchema(): IJSONSchema; + /** + * Notify when the color theme or settings change. + */ + notifyThemeUpdate(theme: IColorTheme): void; + } +type IJSONSchemaForColors = IJSONSchema & { properties: { [name: string]: { oneOf: [IJSONSchemaWithSnippets, IJSONSchema] } } }; +type IJSONSchemaWithSnippets = IJSONSchema & { defaultSnippets: IJSONSchemaSnippet[] }; + class ColorRegistry implements IColorRegistry { private readonly _onDidChangeSchema = new Emitter(); readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; private colorsById: { [key: string]: ColorContribution }; - private colorSchema: IJSONSchema & { properties: IJSONSchemaMap } = { type: 'object', properties: {} }; + private colorSchema: IJSONSchemaForColors = { type: 'object', properties: {} }; private colorReferenceSchema: IJSONSchema & { enum: string[]; enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; constructor() { this.colorsById = {}; } - public registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { + public notifyThemeUpdate(colorThemeData: IColorTheme) { + for (const key of Object.keys(this.colorsById)) { + const color = colorThemeData.getColor(key); + if (color) { + this.colorSchema.properties[key].oneOf[0].defaultSnippets[0].body = `\${1:${color.toString()}}`; + } + } + this._onDidChangeSchema.fire(); + } + + public registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; this.colorsById[id] = colorContribution; - const propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; + const propertySchema: IJSONSchemaWithSnippets = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; if (deprecationMessage) { propertySchema.deprecationMessage = deprecationMessage; } if (needsTransparency) { propertySchema.pattern = '^#(?:(?[0-9a-fA-f]{3}[0-9a-eA-E])|(?:[0-9a-fA-F]{6}(?:(?![fF]{2})(?:[0-9a-fA-F]{2}))))?$'; - propertySchema.patternErrorMessage = 'This color must be transparent or it will obscure content'; + propertySchema.patternErrorMessage = nls.localize('transparecyRequired', 'This color must be transparent or it will obscure content'); } - this.colorSchema.properties[id] = propertySchema; + this.colorSchema.properties[id] = { + oneOf: [ + propertySchema, + { type: 'string', const: DEFAULT_COLOR_CONFIG_VALUE, description: nls.localize('useDefault', 'Use the default color.') } + ] + }; this.colorReferenceSchema.enum.push(id); this.colorReferenceSchema.enumDescriptions.push(description); @@ -169,8 +198,8 @@ class ColorRegistry implements IColorRegistry { public resolveDefaultColor(id: ColorIdentifier, theme: IColorTheme): Color | undefined { const colorDesc = this.colorsById[id]; - if (colorDesc && colorDesc.defaults) { - const colorValue = colorDesc.defaults[theme.type]; + if (colorDesc?.defaults) { + const colorValue = isColorDefaults(colorDesc.defaults) ? colorDesc.defaults[theme.type] : colorDesc.defaults; return resolveColorValue(colorValue, theme); } return undefined; @@ -203,7 +232,7 @@ const colorRegistry = new ColorRegistry(); platform.Registry.add(Extensions.ColorContribution, colorRegistry); -export function registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { +export function registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { return colorRegistry.registerColor(id, defaults, description, needsTransparency, deprecationMessage); } @@ -319,6 +348,7 @@ const schemaRegistry = platform.Registry.as(JSONExten schemaRegistry.registerSchema(workbenchColorsSchemaId, colorRegistry.getColorSchema()); const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(workbenchColorsSchemaId), 200); + colorRegistry.onDidChangeSchema(() => { if (!delayer.isScheduled()) { delayer.schedule(); diff --git a/src/vs/platform/theme/common/colors/baseColors.ts b/src/vs/platform/theme/common/colors/baseColors.ts index 1d19b3adc1f..baf6b86f27f 100644 --- a/src/vs/platform/theme/common/colors/baseColors.ts +++ b/src/vs/platform/theme/common/colors/baseColors.ts @@ -43,7 +43,7 @@ export const activeContrastBorder = registerColor('contrastActiveBorder', nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); export const selectionBackground = registerColor('selection.background', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('selectionBackground', "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.")); diff --git a/src/vs/platform/theme/common/colors/chartsColors.ts b/src/vs/platform/theme/common/colors/chartsColors.ts index eb63b602234..a35e296d2ad 100644 --- a/src/vs/platform/theme/common/colors/chartsColors.ts +++ b/src/vs/platform/theme/common/colors/chartsColors.ts @@ -12,27 +12,27 @@ import { minimapFindMatch } from 'vs/platform/theme/common/colors/minimapColors' export const chartsForeground = registerColor('charts.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('chartsForeground', "The foreground color used in charts.")); export const chartsLines = registerColor('charts.lines', - { dark: transparent(foreground, .5), light: transparent(foreground, .5), hcDark: transparent(foreground, .5), hcLight: transparent(foreground, .5) }, + transparent(foreground, .5), nls.localize('chartsLines', "The color used for horizontal lines in charts.")); export const chartsRed = registerColor('charts.red', - { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + editorErrorForeground, nls.localize('chartsRed', "The red color used in chart visualizations.")); export const chartsBlue = registerColor('charts.blue', - { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + editorInfoForeground, nls.localize('chartsBlue', "The blue color used in chart visualizations.")); export const chartsYellow = registerColor('charts.yellow', - { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + editorWarningForeground, nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); export const chartsOrange = registerColor('charts.orange', - { dark: minimapFindMatch, light: minimapFindMatch, hcDark: minimapFindMatch, hcLight: minimapFindMatch }, + minimapFindMatch, nls.localize('chartsOrange', "The orange color used in chart visualizations.")); export const chartsGreen = registerColor('charts.green', diff --git a/src/vs/platform/theme/common/colors/editorColors.ts b/src/vs/platform/theme/common/colors/editorColors.ts index 4116f5ec141..cebf9ba8f88 100644 --- a/src/vs/platform/theme/common/colors/editorColors.ts +++ b/src/vs/platform/theme/common/colors/editorColors.ts @@ -26,7 +26,7 @@ export const editorForeground = registerColor('editor.foreground', export const editorStickyScrollBackground = registerColor('editorStickyScroll.background', - { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + editorBackground, nls.localize('editorStickyScrollBackground', "Background color of sticky scroll in the editor")); export const editorStickyScrollHoverBackground = registerColor('editorStickyScrollHover.background', @@ -38,7 +38,7 @@ export const editorStickyScrollBorder = registerColor('editorStickyScroll.border nls.localize('editorStickyScrollBorder', "Border color of sticky scroll in the editor")); export const editorStickyScrollShadow = registerColor('editorStickyScroll.shadow', - { dark: scrollbarShadow, light: scrollbarShadow, hcDark: scrollbarShadow, hcLight: scrollbarShadow }, + scrollbarShadow, nls.localize('editorStickyScrollShadow', " Shadow color of sticky scroll in the editor")); @@ -47,7 +47,7 @@ export const editorWidgetBackground = registerColor('editorWidget.background', nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); export const editorWidgetForeground = registerColor('editorWidget.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); export const editorWidgetBorder = registerColor('editorWidget.border', @@ -55,12 +55,12 @@ export const editorWidgetBorder = registerColor('editorWidget.border', nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); export const editorErrorBackground = registerColor('editorError.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorErrorForeground = registerColor('editorError.foreground', @@ -73,7 +73,7 @@ export const editorErrorBorder = registerColor('editorError.border', export const editorWarningBackground = registerColor('editorWarning.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorWarningForeground = registerColor('editorWarning.foreground', @@ -86,7 +86,7 @@ export const editorWarningBorder = registerColor('editorWarning.border', export const editorInfoBackground = registerColor('editorInfo.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorInfoForeground = registerColor('editorInfo.foreground', @@ -141,10 +141,18 @@ export const editorFindMatch = registerColor('editor.findMatchBackground', { light: '#A8AC94', dark: '#515C6A', hcDark: null, hcLight: null }, nls.localize('editorFindMatch', "Color of the current search match.")); +export const editorFindMatchForeground = registerColor('editor.findMatchForeground', + null, + nls.localize('editorFindMatchForeground', "Text color of the current search match.")); + export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', { light: '#EA5C0055', dark: '#EA5C0055', hcDark: null, hcLight: null }, nls.localize('findMatchHighlight', "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations."), true); +export const editorFindMatchHighlightForeground = registerColor('editor.findMatchHighlightForeground', + null, + nls.localize('findMatchHighlightForeground', "Foreground color of the other search matches."), true); + export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', { dark: '#3a3d4166', light: '#b4b4b44d', hcDark: null, hcLight: null }, nls.localize('findRangeHighlight', "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); @@ -169,15 +177,15 @@ export const editorHoverHighlight = registerColor('editor.hoverHighlightBackgrou nls.localize('hoverHighlight', 'Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorHoverBackground = registerColor('editorHoverWidget.background', - { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('hoverBackground', 'Background color of the editor hover.')); export const editorHoverForeground = registerColor('editorHoverWidget.foreground', - { light: editorWidgetForeground, dark: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + editorWidgetForeground, nls.localize('hoverForeground', 'Foreground color of the editor hover.')); export const editorHoverBorder = registerColor('editorHoverWidget.border', - { light: editorWidgetBorder, dark: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, + editorWidgetBorder, nls.localize('hoverBorder', 'Border color of the editor hover.')); export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.statusBarBackground', @@ -196,19 +204,19 @@ export const editorInlayHintBackground = registerColor('editorInlayHint.backgrou nls.localize('editorInlayHintBackground', 'Background color of inline hints')); export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', - { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + editorInlayHintForeground, nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', - { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + editorInlayHintBackground, nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', - { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + editorInlayHintForeground, nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', - { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + editorInlayHintBackground, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); @@ -223,7 +231,7 @@ export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAu nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); export const editorLightBulbAiForeground = registerColor('editorLightBulbAi.foreground', - { dark: editorLightBulbForeground, light: editorLightBulbForeground, hcDark: editorLightBulbForeground, hcLight: editorLightBulbForeground }, + editorLightBulbForeground, nls.localize('editorLightBulbAiForeground', "The color used for the lightbulb AI icon.")); @@ -234,11 +242,11 @@ export const snippetTabstopHighlightBackground = registerColor('editor.snippetTa nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', @@ -270,20 +278,20 @@ export const diffRemovedLine = registerColor('diffEditor.removedLineBackground', export const diffInsertedLineGutter = registerColor('diffEditorGutter.insertedLineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorInsertedLineGutter', 'Background color for the margin where lines got inserted.')); export const diffRemovedLineGutter = registerColor('diffEditorGutter.removedLineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorRemovedLineGutter', 'Background color for the margin where lines got removed.')); export const diffOverviewRulerInserted = registerColor('diffEditorOverview.insertedForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorOverviewInserted', 'Diff overview ruler foreground for inserted content.')); export const diffOverviewRulerRemoved = registerColor('diffEditorOverview.removedForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorOverviewRemoved', 'Diff overview ruler foreground for removed content.')); @@ -306,11 +314,11 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', export const diffUnchangedRegionBackground = registerColor('diffEditor.unchangedRegionBackground', - { dark: 'sideBar.background', light: 'sideBar.background', hcDark: 'sideBar.background', hcLight: 'sideBar.background' }, + 'sideBar.background', nls.localize('diffEditor.unchangedRegionBackground', "The background color of unchanged blocks in the diff editor.")); export const diffUnchangedRegionForeground = registerColor('diffEditor.unchangedRegionForeground', - { dark: 'foreground', light: 'foreground', hcDark: 'foreground', hcLight: 'foreground' }, + 'foreground', nls.localize('diffEditor.unchangedRegionForeground', "The foreground color of unchanged blocks in the diff editor.")); export const diffUnchangedTextBackground = registerColor('diffEditor.unchangedCodeBackground', @@ -347,11 +355,11 @@ export const toolbarActiveBackground = registerColor('toolbar.activeBackground', // ----- breadcumbs export const breadcrumbsForeground = registerColor('breadcrumb.foreground', - { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hcDark: transparent(foreground, 0.8), hcLight: transparent(foreground, 0.8) }, + transparent(foreground, 0.8), nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); export const breadcrumbsBackground = registerColor('breadcrumb.background', - { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + editorBackground, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', @@ -363,7 +371,7 @@ export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.ac nls.localize('breadcrumbsSelectedForeground', "Color of selected breadcrumb items.")); export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', - { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); @@ -381,7 +389,7 @@ export const mergeCurrentHeaderBackground = registerColor('merge.currentHeaderBa nls.localize('mergeCurrentHeaderBackground', 'Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCurrentContentBackground = registerColor('merge.currentContentBackground', - { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, + transparent(mergeCurrentHeaderBackground, contentTransparency), nls.localize('mergeCurrentContentBackground', 'Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeaderBackground', @@ -389,7 +397,7 @@ export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeader nls.localize('mergeIncomingHeaderBackground', 'Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeIncomingContentBackground = registerColor('merge.incomingContentBackground', - { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, + transparent(mergeIncomingHeaderBackground, contentTransparency), nls.localize('mergeIncomingContentBackground', 'Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBackground', @@ -397,7 +405,7 @@ export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBack nls.localize('mergeCommonHeaderBackground', 'Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCommonContentBackground = registerColor('merge.commonContentBackground', - { dark: transparent(mergeCommonHeaderBackground, contentTransparency), light: transparent(mergeCommonHeaderBackground, contentTransparency), hcDark: transparent(mergeCommonHeaderBackground, contentTransparency), hcLight: transparent(mergeCommonHeaderBackground, contentTransparency) }, + transparent(mergeCommonHeaderBackground, contentTransparency), nls.localize('mergeCommonContentBackground', 'Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeBorder = registerColor('merge.border', @@ -418,24 +426,24 @@ export const overviewRulerCommonContentForeground = registerColor('editorOvervie nls.localize('overviewRulerCommonContentForeground', 'Common ancestor overview ruler foreground for inline merge-conflicts.')); export const overviewRulerFindMatchForeground = registerColor('editorOverviewRuler.findMatchForeground', - { dark: '#d186167e', light: '#d186167e', hcDark: '#AB5A00', hcLight: '' }, + { dark: '#d186167e', light: '#d186167e', hcDark: '#AB5A00', hcLight: '#AB5A00' }, nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', - { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, + '#A0A0A0CC', nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); // ----- problems export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', - { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + editorErrorForeground, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', - { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + editorWarningForeground, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', - { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + editorInfoForeground, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); diff --git a/src/vs/platform/theme/common/colors/inputColors.ts b/src/vs/platform/theme/common/colors/inputColors.ts index dc38222d402..f31b804f7a1 100644 --- a/src/vs/platform/theme/common/colors/inputColors.ts +++ b/src/vs/platform/theme/common/colors/inputColors.ts @@ -21,7 +21,7 @@ export const inputBackground = registerColor('input.background', nls.localize('inputBoxBackground', "Input box background.")); export const inputForeground = registerColor('input.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('inputBoxForeground', "Input box foreground.")); export const inputBorder = registerColor('input.border', @@ -110,11 +110,11 @@ export const selectBorder = registerColor('dropdown.border', // ------ button export const buttonForeground = registerColor('button.foreground', - { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, + Color.white, nls.localize('buttonForeground', "Button foreground color.")); export const buttonSeparator = registerColor('button.separator', - { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: transparent(buttonForeground, .4), hcLight: transparent(buttonForeground, .4) }, + transparent(buttonForeground, .4), nls.localize('buttonSeparator', "Button separator color.")); export const buttonBackground = registerColor('button.background', @@ -126,7 +126,7 @@ export const buttonHoverBackground = registerColor('button.hoverBackground', nls.localize('buttonHoverBackground', "Button background color when hovering.")); export const buttonBorder = registerColor('button.border', - { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, + contrastBorder, nls.localize('buttonBorder', "Button border color.")); export const buttonSecondaryForeground = registerColor('button.secondaryForeground', @@ -141,27 +141,56 @@ export const buttonSecondaryHoverBackground = registerColor('button.secondaryHov { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); +// ------ radio + +export const radioActiveForeground = registerColor('radio.activeForeground', + inputActiveOptionForeground, + nls.localize('radioActiveForeground', "Foreground color of active radio option.")); + +export const radioActiveBackground = registerColor('radio.activeBackground', + inputActiveOptionBackground, + nls.localize('radioBackground', "Background color of active radio option.")); + +export const radioActiveBorder = registerColor('radio.activeBorder', + inputActiveOptionBorder, + nls.localize('radioActiveBorder', "Border color of the active radio option.")); + +export const radioInactiveForeground = registerColor('radio.inactiveForeground', + null, + nls.localize('radioInactiveForeground', "Foreground color of inactive radio option.")); + +export const radioInactiveBackground = registerColor('radio.inactiveBackground', + null, + nls.localize('radioInactiveBackground', "Background color of inactive radio option.")); + +export const radioInactiveBorder = registerColor('radio.inactiveBorder', + { light: transparent(radioActiveForeground, .2), dark: transparent(radioActiveForeground, .2), hcDark: transparent(radioActiveForeground, .4), hcLight: transparent(radioActiveForeground, .2) }, + nls.localize('radioInactiveBorder', "Border color of the inactive radio option.")); + +export const radioInactiveHoverBackground = registerColor('radio.inactiveHoverBackground', + inputActiveOptionHoverBackground, + nls.localize('radioHoverBackground', "Background color of inactive active radio option when hovering.")); // ------ checkbox export const checkboxBackground = registerColor('checkbox.background', - { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + selectBackground, nls.localize('checkbox.background', "Background color of checkbox widget.")); export const checkboxSelectBackground = registerColor('checkbox.selectBackground', - { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('checkbox.select.background', "Background color of checkbox widget when the element it's in is selected.")); export const checkboxForeground = registerColor('checkbox.foreground', - { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + selectForeground, nls.localize('checkbox.foreground', "Foreground color of checkbox widget.")); export const checkboxBorder = registerColor('checkbox.border', - { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, + selectBorder, nls.localize('checkbox.border', "Border color of checkbox widget.")); export const checkboxSelectBorder = registerColor('checkbox.selectBorder', - { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, + iconForeground, nls.localize('checkbox.select.border', "Border color of checkbox widget when the element it's in is selected.")); diff --git a/src/vs/platform/theme/common/colors/listColors.ts b/src/vs/platform/theme/common/colors/listColors.ts index e2c957af94d..dd5c405199c 100644 --- a/src/vs/platform/theme/common/colors/listColors.ts +++ b/src/vs/platform/theme/common/colors/listColors.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; // Import the effects we need import { Color } from 'vs/base/common/color'; -import { registerColor, darken, lighten, transparent } from 'vs/platform/theme/common/colorUtils'; +import { registerColor, darken, lighten, transparent, ifDefinedThenElse } from 'vs/platform/theme/common/colorUtils'; // Import the colors we need import { foreground, contrastBorder, activeContrastBorder, focusBorder, iconForeground } from 'vs/platform/theme/common/colors/baseColors'; @@ -15,11 +15,11 @@ import { editorWidgetBackground, editorFindMatchHighlightBorder, editorFindMatch export const listFocusBackground = registerColor('list.focusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusForeground = registerColor('list.focusForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusOutline = registerColor('list.focusOutline', @@ -27,7 +27,7 @@ export const listFocusOutline = registerColor('list.focusOutline', nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusAndSelectionOutline = registerColor('list.focusAndSelectionOutline', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusAndSelectionOutline', "List/Tree outline color for the focused item when the list/tree is active and selected. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', @@ -39,7 +39,7 @@ export const listActiveSelectionForeground = registerColor('list.activeSelection nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', @@ -47,19 +47,19 @@ export const listInactiveSelectionBackground = registerColor('list.inactiveSelec nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listHoverBackground = registerColor('list.hoverBackground', @@ -67,7 +67,7 @@ export const listHoverBackground = registerColor('list.hoverBackground', nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropOverBackground = registerColor('list.dropBackground', @@ -83,7 +83,7 @@ export const listHighlightForeground = registerColor('list.highlightForeground', nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); export const listFocusHighlightForeground = registerColor('list.focusHighlightForeground', - { dark: listHighlightForeground, light: listHighlightForeground, hcDark: listHighlightForeground, hcLight: listHighlightForeground }, + { dark: listHighlightForeground, light: ifDefinedThenElse(listActiveSelectionBackground, listHighlightForeground, '#BBE7FF'), hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('listFocusHighlightForeground', 'List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.')); export const listInvalidItemForeground = registerColor('list.invalidItemForeground', @@ -109,7 +109,7 @@ export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget. nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); export const listFilterWidgetShadow = registerColor('listFilterWidget.shadow', - { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, + widgetShadow, nls.localize('listFilterWidgetShadow', 'Shadow color of the type filter widget in lists and trees.')); export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', @@ -132,7 +132,7 @@ export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); export const treeInactiveIndentGuidesStroke = registerColor('tree.inactiveIndentGuidesStroke', - { dark: transparent(treeIndentGuidesStroke, 0.4), light: transparent(treeIndentGuidesStroke, 0.4), hcDark: transparent(treeIndentGuidesStroke, 0.4), hcLight: transparent(treeIndentGuidesStroke, 0.4) }, + transparent(treeIndentGuidesStroke, 0.4), nls.localize('treeInactiveIndentGuidesStroke', "Tree stroke color for the indentation guides that are not active.")); diff --git a/src/vs/platform/theme/common/colors/menuColors.ts b/src/vs/platform/theme/common/colors/menuColors.ts index 6fa9a0ec326..05bf5491952 100644 --- a/src/vs/platform/theme/common/colors/menuColors.ts +++ b/src/vs/platform/theme/common/colors/menuColors.ts @@ -19,19 +19,19 @@ export const menuBorder = registerColor('menu.border', nls.localize('menuBorder', "Border color of menus.")); export const menuForeground = registerColor('menu.foreground', - { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + selectForeground, nls.localize('menuForeground', "Foreground color of menu items.")); export const menuBackground = registerColor('menu.background', - { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + selectBackground, nls.localize('menuBackground', "Background color of menu items.")); export const menuSelectionForeground = registerColor('menu.selectionForeground', - { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + listActiveSelectionForeground, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); export const menuSelectionBackground = registerColor('menu.selectionBackground', - { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hcDark: listActiveSelectionBackground, hcLight: listActiveSelectionBackground }, + listActiveSelectionBackground, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); export const menuSelectionBorder = registerColor('menu.selectionBorder', diff --git a/src/vs/platform/theme/common/colors/minimapColors.ts b/src/vs/platform/theme/common/colors/minimapColors.ts index 0b051994d09..ade38578c28 100644 --- a/src/vs/platform/theme/common/colors/minimapColors.ts +++ b/src/vs/platform/theme/common/colors/minimapColors.ts @@ -39,21 +39,21 @@ export const minimapError = registerColor('minimap.errorHighlight', nls.localize('minimapError', 'Minimap marker color for errors.')); export const minimapBackground = registerColor('minimap.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('minimapBackground', "Minimap background color.")); export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', - { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hcDark: Color.fromHex('#000f'), hcLight: Color.fromHex('#000f') }, + Color.fromHex('#000f'), nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); export const minimapSliderBackground = registerColor('minimapSlider.background', - { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hcDark: transparent(scrollbarSliderBackground, 0.5), hcLight: transparent(scrollbarSliderBackground, 0.5) }, + transparent(scrollbarSliderBackground, 0.5), nls.localize('minimapSliderBackground', "Minimap slider background color.")); export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', - { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hcDark: transparent(scrollbarSliderHoverBackground, 0.5), hcLight: transparent(scrollbarSliderHoverBackground, 0.5) }, + transparent(scrollbarSliderHoverBackground, 0.5), nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', - { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hcDark: transparent(scrollbarSliderActiveBackground, 0.5), hcLight: transparent(scrollbarSliderActiveBackground, 0.5) }, + transparent(scrollbarSliderActiveBackground, 0.5), nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); diff --git a/src/vs/platform/theme/common/colors/miscColors.ts b/src/vs/platform/theme/common/colors/miscColors.ts index 5a2ea49b702..42a00e23e6a 100644 --- a/src/vs/platform/theme/common/colors/miscColors.ts +++ b/src/vs/platform/theme/common/colors/miscColors.ts @@ -16,7 +16,7 @@ import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colors/bas // ----- sash export const sashHoverBorder = registerColor('sash.hoverBorder', - { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, + focusBorder, nls.localize('sashActiveBorder', "Border color of active sashes.")); diff --git a/src/vs/platform/theme/common/colors/quickpickColors.ts b/src/vs/platform/theme/common/colors/quickpickColors.ts index 7f8fc271a6e..3b109a21872 100644 --- a/src/vs/platform/theme/common/colors/quickpickColors.ts +++ b/src/vs/platform/theme/common/colors/quickpickColors.ts @@ -15,11 +15,11 @@ import { listActiveSelectionBackground, listActiveSelectionForeground, listActiv export const quickInputBackground = registerColor('quickInput.background', - { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputForeground = registerColor('quickInput.foreground', - { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + editorWidgetForeground, nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputTitleBackground = registerColor('quickInputTitle.background', @@ -35,15 +35,15 @@ export const pickerGroupBorder = registerColor('pickerGroup.border', nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, '', undefined, + null, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', - { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + listActiveSelectionForeground, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); export const quickInputListFocusIconForeground = registerColor('quickInputList.focusIconForeground', - { dark: listActiveSelectionIconForeground, light: listActiveSelectionIconForeground, hcDark: listActiveSelectionIconForeground, hcLight: listActiveSelectionIconForeground }, + listActiveSelectionIconForeground, nls.localize('quickInput.listFocusIconForeground', "Quick picker icon foreground color for the focused item.")); export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index 1b9511f46cb..e332feed264 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, nativeTheme } from 'electron'; +import electron from 'electron'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -22,6 +22,11 @@ const THEME_STORAGE_KEY = 'theme'; const THEME_BG_STORAGE_KEY = 'themeBackground'; const THEME_WINDOW_SPLASH = 'windowSplash'; +namespace ThemeSettings { + export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme'; + export const SYSTEM_COLOR_THEME = 'window.systemColorTheme'; +} + export const IThemeMainService = createDecorator('themeMainService'); export interface IThemeMainService { @@ -49,58 +54,66 @@ export class ThemeMainService extends Disposable implements IThemeMainService { super(); // System Theme - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('window.systemColorTheme')) { - this.updateSystemColorTheme(); - } - })); + if (!isLinux) { + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ThemeSettings.SYSTEM_COLOR_THEME) || e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME)) { + this.updateSystemColorTheme(); + } + })); + } this.updateSystemColorTheme(); // Color Scheme changes - this._register(Event.fromNodeEventEmitter(nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme()))); + this._register(Event.fromNodeEventEmitter(electron.nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme()))); } private updateSystemColorTheme(): void { - switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>('window.systemColorTheme')) { - case 'dark': - nativeTheme.themeSource = 'dark'; - break; - case 'light': - nativeTheme.themeSource = 'light'; - break; - case 'auto': - switch (this.getBaseTheme()) { - case 'vs': nativeTheme.themeSource = 'light'; break; - case 'vs-dark': nativeTheme.themeSource = 'dark'; break; - default: nativeTheme.themeSource = 'system'; - } - break; - default: - nativeTheme.themeSource = 'system'; - break; + if (isLinux || this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { + // only with `system` we can detect the system color scheme + electron.nativeTheme.themeSource = 'system'; + } else { + switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>(ThemeSettings.SYSTEM_COLOR_THEME)) { + case 'dark': + electron.nativeTheme.themeSource = 'dark'; + break; + case 'light': + electron.nativeTheme.themeSource = 'light'; + break; + case 'auto': + switch (this.getBaseTheme()) { + case 'vs': electron.nativeTheme.themeSource = 'light'; break; + case 'vs-dark': electron.nativeTheme.themeSource = 'dark'; break; + default: electron.nativeTheme.themeSource = 'system'; + } + break; + default: + electron.nativeTheme.themeSource = 'system'; + break; + } + } } getColorScheme(): IColorScheme { if (isWindows) { // high contrast is refelected by the shouldUseInvertedColorScheme property - if (nativeTheme.shouldUseHighContrastColors) { + if (electron.nativeTheme.shouldUseHighContrastColors) { // shouldUseInvertedColorScheme is dark, !shouldUseInvertedColorScheme is light - return { dark: nativeTheme.shouldUseInvertedColorScheme, highContrast: true }; + return { dark: electron.nativeTheme.shouldUseInvertedColorScheme, highContrast: true }; } } else if (isMacintosh) { // high contrast is set if one of shouldUseInvertedColorScheme or shouldUseHighContrastColors is set, reflecting the 'Invert colours' and `Increase contrast` settings in MacOS - if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) { - return { dark: nativeTheme.shouldUseDarkColors, highContrast: true }; + if (electron.nativeTheme.shouldUseInvertedColorScheme || electron.nativeTheme.shouldUseHighContrastColors) { + return { dark: electron.nativeTheme.shouldUseDarkColors, highContrast: true }; } } else if (isLinux) { // ubuntu gnome seems to have 3 states, light dark and high contrast - if (nativeTheme.shouldUseHighContrastColors) { + if (electron.nativeTheme.shouldUseHighContrastColors) { return { dark: true, highContrast: true }; } } return { - dark: nativeTheme.shouldUseDarkColors, + dark: electron.nativeTheme.shouldUseDarkColors, highContrast: false }; } @@ -157,7 +170,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { - for (const window of BrowserWindow.getAllWindows()) { + for (const window of electron.BrowserWindow.getAllWindows()) { if (window.id === windowId) { window.setBackgroundColor(splash.colorInfo.background); break; diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 86b4da4b409..b1433f2e745 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -215,7 +215,7 @@ export class DisposableTunnel { } } -export abstract class AbstractTunnelService implements ITunnelService { +export abstract class AbstractTunnelService extends Disposable implements ITunnelService { declare readonly _serviceBrand: undefined; private _onTunnelOpened: Emitter = new Emitter(); @@ -234,7 +234,7 @@ export abstract class AbstractTunnelService implements ITunnelService { public constructor( @ILogService protected readonly logService: ILogService, @IConfigurationService protected readonly configurationService: IConfigurationService - ) { } + ) { super(); } get hasTunnelProvider(): boolean { return !!this._tunnelProvider; @@ -308,7 +308,8 @@ export abstract class AbstractTunnelService implements ITunnelService { return tunnels; } - async dispose(): Promise { + override async dispose(): Promise { + super.dispose(); for (const portMap of this._tunnels.values()) { for (const { value } of portMap.values()) { await value.then(tunnel => typeof tunnel !== 'string' ? tunnel?.dispose() : undefined); diff --git a/src/vs/platform/tunnel/test/common/tunnel.test.ts b/src/vs/platform/tunnel/test/common/tunnel.test.ts index d86d3f47bd7..ae32707eb30 100644 --- a/src/vs/platform/tunnel/test/common/tunnel.test.ts +++ b/src/vs/platform/tunnel/test/common/tunnel.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { extractLocalHostUriMetaDataForPortMapping, diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index 27d69e4e5ac..32d33a7d23c 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 73e7d7afffe..18cafc9196d 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -76,12 +76,12 @@ export const State = { Uninitialized: { type: StateType.Uninitialized } as Uninitialized, Disabled: (reason: DisablementReason) => ({ type: StateType.Disabled, reason }) as Disabled, Idle: (updateType: UpdateType, error?: string) => ({ type: StateType.Idle, updateType, error }) as Idle, - CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), - AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), + CheckingForUpdates: (explicit: boolean): CheckingForUpdates => ({ type: StateType.CheckingForUpdates, explicit }), + AvailableForDownload: (update: IUpdate): AvailableForDownload => ({ type: StateType.AvailableForDownload, update }), Downloading: { type: StateType.Downloading } as Downloading, - Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), - Updating: (update: IUpdate) => ({ type: StateType.Updating, update } as Updating), - Ready: (update: IUpdate) => ({ type: StateType.Ready, update } as Ready), + Downloaded: (update: IUpdate): Downloaded => ({ type: StateType.Downloaded, update }), + Updating: (update: IUpdate): Updating => ({ type: StateType.Updating, update }), + Ready: (update: IUpdate): Ready => ({ type: StateType.Ready, update }), }; export interface IAutoUpdater extends Event.NodeEventEmitter { diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 39e65d4a2dd..ea18f4ab5a6 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -20,7 +20,7 @@ export function createUpdateURL(platform: string, quality: string, productServic export type UpdateNotAvailableClassification = { owner: 'joaomoreno'; - explicit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the user has manually checked for updates, or this was an automatic check.' }; + explicit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user has manually checked for updates, or this was an automatic check.' }; comment: 'This is used to understand how often VS Code pings the update server for an update and there\'s none available.'; }; diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 4c49a758185..a2561be0c11 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -55,7 +55,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun @memoize get cachePath(): Promise { const result = path.join(tmpdir(), `vscode-${this.productService.quality}-${this.productService.target}-${process.arch}`); - return pfs.Promises.mkdir(result, { recursive: true }).then(() => result); + return fs.promises.mkdir(result, { recursive: true }).then(() => result); } constructor( @@ -197,7 +197,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const promises = versions.filter(filter).map(async one => { try { - await pfs.Promises.unlink(path.join(cachePath, one)); + await fs.promises.unlink(path.join(cachePath, one)); } catch (err) { // ignore } diff --git a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts index 32dde9e11d7..339e86bf938 100644 --- a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts +++ b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { mock } from 'vs/base/test/common/mock'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; diff --git a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts index b6a05ce558f..8f0c5641d9f 100644 --- a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts +++ b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index f18de2499cd..f18ae097050 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -35,6 +35,7 @@ export const enum ProfileResourceType { * Flags to indicate whether to use the default profile or not. */ export type UseDefaultProfileFlags = { [key in ProfileResourceType]?: boolean }; +export type ProfileResourceTypeFlags = UseDefaultProfileFlags; export interface IUserDataProfile { readonly id: string; @@ -167,6 +168,10 @@ export type UserDataProfilesObject = { emptyWindows: Map; }; +type TransientUserDataProfilesObject = UserDataProfilesObject & { + folders: ResourceMap; +}; + export type StoredUserDataProfile = { name: string; location: URI; @@ -208,8 +213,9 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf private profileCreationPromises = new Map>(); - protected readonly transientProfilesObject: UserDataProfilesObject = { + protected readonly transientProfilesObject: TransientUserDataProfilesObject = { profiles: [], + folders: new ResourceMap(), workspaces: new ResourceMap(), emptyWindows: new Map() }; @@ -335,7 +341,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf try { const existing = this.profiles.find(p => p.name === name || p.id === id); if (existing) { - return existing; + throw new Error(`Profile with ${name} name already exists`); } const profile = toUserDataProfile(id, name, joinPath(this.profilesHome, id), this.profilesCacheHome, options, this.defaultProfile); @@ -453,6 +459,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } async resetWorkspaces(): Promise { + this.transientProfilesObject.folders.clear(); this.transientProfilesObject.workspaces.clear(); this.transientProfilesObject.emptyWindows.clear(); this.profilesObject.workspaces.clear(); @@ -483,7 +490,17 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf getProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): IUserDataProfile | undefined { const workspace = this.getWorkspace(workspaceIdentifier); - return URI.isUri(workspace) ? this.transientProfilesObject.workspaces.get(workspace) ?? this.profilesObject.workspaces.get(workspace) : this.transientProfilesObject.emptyWindows.get(workspace) ?? this.profilesObject.emptyWindows.get(workspace); + const profile = URI.isUri(workspace) ? this.profilesObject.workspaces.get(workspace) : this.profilesObject.emptyWindows.get(workspace); + if (profile) { + return profile; + } + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { + return this.transientProfilesObject.folders.get(workspaceIdentifier.uri); + } + if (isWorkspaceIdentifier(workspaceIdentifier)) { + return this.transientProfilesObject.workspaces.get(workspaceIdentifier.configPath); + } + return this.transientProfilesObject.emptyWindows.get(workspaceIdentifier.id); } protected getWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): URI | string { @@ -497,16 +514,19 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } private isProfileAssociatedToWorkspace(profile: IUserDataProfile): boolean { + if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { + return true; + } + if ([...this.profilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + return true; + } if ([...this.transientProfilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { return true; } if ([...this.transientProfilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { return true; } - if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { - return true; - } - if ([...this.profilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + if ([...this.transientProfilesObject.folders.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { return true; } return false; @@ -515,6 +535,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]): void { const allProfiles = [...this.profiles, ...added]; const storedProfiles: StoredUserDataProfile[] = []; + const transientProfiles = this.transientProfilesObject.profiles; this.transientProfilesObject.profiles = []; for (let profile of allProfiles) { if (profile.isDefault) { @@ -524,9 +545,30 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf continue; } profile = updated.find(p => profile.id === p.id) ?? profile; + const transientProfile = transientProfiles.find(p => profile.id === p.id); if (profile.isTransient) { this.transientProfilesObject.profiles.push(profile); } else { + if (transientProfile) { + for (const [windowId, p] of this.transientProfilesObject.emptyWindows.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: windowId }, profile); + break; + } + } + for (const [workspace, p] of this.transientProfilesObject.workspaces.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: '', configPath: workspace }, profile); + break; + } + } + for (const [folder, p] of this.transientProfilesObject.folders.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: '', uri: folder }, profile); + break; + } + } + } storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } @@ -543,30 +585,48 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf // Force transient if the new profile to associate is transient transient = newProfile?.isTransient ? true : transient; - if (!transient) { + if (transient) { + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.folders.delete(workspaceIdentifier.uri); + if (newProfile) { + this.transientProfilesObject.folders.set(workspaceIdentifier.uri, newProfile); + } + } + + else if (isWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.workspaces.delete(workspaceIdentifier.configPath); + if (newProfile) { + this.transientProfilesObject.workspaces.set(workspaceIdentifier.configPath, newProfile); + } + } + + else { + this.transientProfilesObject.emptyWindows.delete(workspaceIdentifier.id); + if (newProfile) { + this.transientProfilesObject.emptyWindows.set(workspaceIdentifier.id, newProfile); + } + } + } + + else { // Unset the transiet workspace association if any this.updateWorkspaceAssociation(workspaceIdentifier, undefined, true); - } + const workspace = this.getWorkspace(workspaceIdentifier); - const workspace = this.getWorkspace(workspaceIdentifier); - const profilesObject = transient ? this.transientProfilesObject : this.profilesObject; - - // Folder or Multiroot workspace - if (URI.isUri(workspace)) { - profilesObject.workspaces.delete(workspace); - if (newProfile) { - profilesObject.workspaces.set(workspace, newProfile); + // Folder or Multiroot workspace + if (URI.isUri(workspace)) { + this.profilesObject.workspaces.delete(workspace); + if (newProfile) { + this.profilesObject.workspaces.set(workspace, newProfile); + } } - } - // Empty Window - else { - profilesObject.emptyWindows.delete(workspace); - if (newProfile) { - profilesObject.emptyWindows.set(workspace, newProfile); + // Empty Window + else { + this.profilesObject.emptyWindows.delete(workspace); + if (newProfile) { + this.profilesObject.emptyWindows.set(workspace, newProfile); + } } - } - - if (!transient) { this.updateStoredProfileAssociations(); } } diff --git a/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts index a04c44f96ef..a9a7b3771c7 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, MutableDisposable, isDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, MutableDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IStorage, IStorageDatabase, Storage } from 'vs/base/parts/storage/common/storage'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { AbstractStorageService, IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget, isProfileUsingDefaultStorage } from 'vs/platform/storage/common/storage'; @@ -63,10 +63,16 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i readonly abstract onDidChange: Event; + private readonly storageServicesMap: DisposableMap | undefined; + constructor( + persistStorages: boolean, @IStorageService protected readonly storageService: IStorageService ) { super(); + if (persistStorages) { + this.storageServicesMap = this._register(new DisposableMap()); + } } async readStorageData(profile: IUserDataProfile): Promise> { @@ -82,16 +88,30 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i return fn(this.storageService); } - const storageDatabase = await this.createStorageDatabase(profile); - const storageService = new StorageService(storageDatabase); + let storageService = this.storageServicesMap?.get(profile.id); + if (!storageService) { + storageService = new StorageService(this.createStorageDatabase(profile)); + this.storageServicesMap?.set(profile.id, storageService); + + try { + await storageService.initialize(); + } catch (error) { + if (this.storageServicesMap?.has(profile.id)) { + this.storageServicesMap.deleteAndDispose(profile.id); + } else { + storageService.dispose(); + } + throw error; + } + } try { - await storageService.initialize(); const result = await fn(storageService); await storageService.flush(); return result; } finally { - storageService.dispose(); - await this.closeAndDispose(storageDatabase); + if (!this.storageServicesMap?.has(profile.id)) { + storageService.dispose(); + } } } @@ -111,16 +131,6 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i storageService.storeAll(Array.from(items.entries()).map(([key, value]) => ({ key, value, scope: StorageScope.PROFILE, target })), true); } - protected async closeAndDispose(storageDatabase: IStorageDatabase): Promise { - try { - await storageDatabase.close(); - } finally { - if (isDisposable(storageDatabase)) { - storageDatabase.dispose(); - } - } - } - protected abstract createStorageDatabase(profile: IUserDataProfile): Promise; } @@ -130,12 +140,13 @@ export class RemoteUserDataProfileStorageService extends AbstractUserDataProfile readonly onDidChange: Event; constructor( + persistStorages: boolean, private readonly remoteService: IRemoteService, userDataProfilesService: IUserDataProfilesService, storageService: IStorageService, logService: ILogService, ) { - super(storageService); + super(persistStorages, storageService); const channel = remoteService.getChannel('profileStorageListener'); const disposable = this._register(new MutableDisposable()); @@ -164,14 +175,26 @@ export class RemoteUserDataProfileStorageService extends AbstractUserDataProfile class StorageService extends AbstractStorageService { - private readonly profileStorage: IStorage; + private profileStorage: IStorage | undefined; - constructor(profileStorageDatabase: IStorageDatabase) { + constructor(private readonly profileStorageDatabase: Promise) { super({ flushInterval: 100 }); - this.profileStorage = this._register(new Storage(profileStorageDatabase)); } - protected doInitialize(): Promise { + protected async doInitialize(): Promise { + const profileStorageDatabase = await this.profileStorageDatabase; + const profileStorage = new Storage(profileStorageDatabase); + this._register(profileStorage.onDidChangeStorage(e => { + this.emitDidChangeValue(StorageScope.PROFILE, e); + })); + this._register(toDisposable(() => { + profileStorage.close(); + profileStorage.dispose(); + if (isDisposable(profileStorageDatabase)) { + profileStorageDatabase.dispose(); + } + })); + this.profileStorage = profileStorage; return this.profileStorage.init(); } diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts index 0679efcc742..313bedc9bff 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts @@ -18,7 +18,7 @@ export class NativeUserDataProfileStorageService extends RemoteUserDataProfileSt @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { - super(mainProcessService, userDataProfilesService, storageService, logService); + super(false, mainProcessService, userDataProfilesService, storageService, logService); } } diff --git a/src/vs/platform/userDataProfile/node/userDataProfile.ts b/src/vs/platform/userDataProfile/node/userDataProfile.ts index b5fc85922e9..e8de67d7241 100644 --- a/src/vs/platform/userDataProfile/node/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/node/userDataProfile.ts @@ -93,7 +93,7 @@ export class UserDataProfilesService extends UserDataProfilesReadonlyService imp result[URI.revive(workspace).toString()] = URI.revive(profile).toString(); return result; }, {}); - this.stateService.setItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_KEY, { workspaces }); + this.stateService.setItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_KEY, { workspaces } satisfies StoredProfileAssociations); } const associations = super.getStoredProfileAssociations(); if (!this.stateService.getItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_MIGRATION_KEY, false)) { diff --git a/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts index 3b37d056aae..703011c9d60 100644 --- a/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts @@ -9,7 +9,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { RemoteUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; -export class NativeUserDataProfileStorageService extends RemoteUserDataProfileStorageService { +export class SharedProcessUserDataProfileStorageService extends RemoteUserDataProfileStorageService { constructor( @IMainProcessService mainProcessService: IMainProcessService, @@ -17,6 +17,6 @@ export class NativeUserDataProfileStorageService extends RemoteUserDataProfileSt @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { - super(mainProcessService, userDataProfilesService, storageService, logService); + super(true, mainProcessService, userDataProfilesService, storageService, logService); } } diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 7e898dd15fa..a7e2da87458 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts index 48a68ee98a1..36cfb4efce5 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { InMemoryStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; @@ -43,7 +43,6 @@ export class TestUserDataProfileStorageService extends AbstractUserDataProfileSt return this.createStorageDatabase(profile); } - protected override async closeAndDispose(): Promise { } } suite('ProfileStorageService', () => { @@ -54,7 +53,7 @@ suite('ProfileStorageService', () => { let storage: Storage; setup(async () => { - testObject = disposables.add(new TestUserDataProfileStorageService(disposables.add(new InMemoryStorageService()))); + testObject = disposables.add(new TestUserDataProfileStorageService(false, disposables.add(new InMemoryStorageService()))); storage = disposables.add(new Storage(await testObject.setupStorageDatabase(profile))); await storage.init(); }); diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 515b2ce80da..8797fafc6c4 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index eb24d9ca6b5..b62a1be3db8 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -32,7 +32,7 @@ import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userData type IncompatibleSyncSourceClassification = { owner: 'sandy081'; comment: 'Information about the sync resource that is incompatible'; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'settings sync resource. eg., settings, keybindings...' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'settings sync resource. eg., settings, keybindings...' }; }; export function isRemoteUserData(thing: any): thing is IRemoteUserData { @@ -592,7 +592,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa return { syncResource: this.resource, profile: this.syncResource.profile, remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine: isRemoteDataFromCurrentMachine }; } - async getLastSyncUserData(): Promise { + async getLastSyncUserData(): Promise { let storedLastSyncUserDataStateContent = this.getStoredLastSyncUserDataStateContent(); if (!storedLastSyncUserDataStateContent) { storedLastSyncUserDataStateContent = await this.migrateLastSyncUserData(); @@ -664,7 +664,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa return { ...lastSyncUserDataState, syncData, - } as T; + }; } protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index be0c0e0bbb4..7af2df9134a 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { ExtensionType, IExtensionIdentifier, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions'; @@ -483,10 +483,11 @@ export class LocalExtensionsProvider { isMachineScoped: false /* set isMachineScoped value to prevent install and sync dialog in web */, donotIncludePackAndDependencies: true, installGivenVersion: e.pinned && !!e.version, + pinned: e.pinned, installPreReleaseVersion: e.preRelease, profileLocation: profile.extensionsResource, isApplicationScoped: e.isApplicationScoped, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SYNC_CONTEXT]: true } + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.SETTINGS_SYNC } } }); syncExtensionsToInstall.set(extension.identifier.id.toLowerCase(), e); @@ -532,7 +533,7 @@ export class LocalExtensionsProvider { addToSkipped.push(e); this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, gallery.displayName || gallery.identifier.id); } - if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) { + if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) { this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, gallery.displayName || gallery.identifier.id); } else if (error) { this.logService.error(error); @@ -569,7 +570,7 @@ export class LocalExtensionsProvider { return this.userDataProfileStorageService.withProfileScopedStorageService(profile, async storageService => { const disposables = new DisposableStore(); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService]))); const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService)); const extensionStorageService = disposables.add(instantiationService.createInstance(ExtensionStorageService)); try { diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 4c698553970..7a28512a252 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -244,7 +244,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs if (remoteChange !== Change.None) { // update remote this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`); - const content = JSON.stringify({ storage: remote.all }); + const content = JSON.stringify({ storage: remote.all }); remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref); this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state.${remote.added.length ? ` Added: ${remote.added}.` : ''}${remote.updated.length ? ` Updated: ${remote.updated}.` : ''}${remote.removed.length ? ` Removed: ${remote.removed}.` : ''}`); } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index e16e0073a28..c9499d33e9e 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { distinct } from 'vs/base/common/arrays'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; @@ -12,6 +13,7 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -19,7 +21,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { getIgnoredSettings, isEmpty, merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getIgnoredSettingsForExtension } from 'vs/platform/userDataSync/common/userDataSync'; interface ISettingsResourcePreview extends IFileResourcePreview { previewResult: IMergeResult; @@ -51,7 +53,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); constructor( - profile: IUserDataProfile, + private readonly profile: IUserDataProfile, collection: string | undefined, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @@ -73,7 +75,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - const parser = new ConfigurationModelParser(USER_DATA_SYNC_CONFIGURATION_SCOPE); + const parser = new ConfigurationModelParser(USER_DATA_SYNC_CONFIGURATION_SCOPE, this.logService); if (remoteSettingsSyncContent?.settings) { parser.parse(remoteSettingsSyncContent.settings); } @@ -315,21 +317,39 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return { settings }; } - private _defaultIgnoredSettings: Promise | undefined = undefined; + private coreIgnoredSettings: Promise | undefined = undefined; + private systemExtensionsIgnoredSettings: Promise | undefined = undefined; + private userExtensionsIgnoredSettings: Promise | undefined = undefined; private async getIgnoredSettings(content?: string): Promise { - if (!this._defaultIgnoredSettings) { - this._defaultIgnoredSettings = this.userDataSyncUtilService.resolveDefaultIgnoredSettings(); + if (!this.coreIgnoredSettings) { + this.coreIgnoredSettings = this.userDataSyncUtilService.resolveDefaultCoreIgnoredSettings(); + } + if (!this.systemExtensionsIgnoredSettings) { + this.systemExtensionsIgnoredSettings = this.getIgnoredSettingForSystemExtensions(); + } + if (!this.userExtensionsIgnoredSettings) { + this.userExtensionsIgnoredSettings = this.getIgnoredSettingForUserExtensions(); const disposable = this._register(Event.any( Event.filter(this.extensionManagementService.onDidInstallExtensions, (e => e.some(({ local }) => !!local))), Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)))(() => { disposable.dispose(); - this._defaultIgnoredSettings = undefined; + this.userExtensionsIgnoredSettings = undefined; })); } - const defaultIgnoredSettings = await this._defaultIgnoredSettings; + const defaultIgnoredSettings = (await Promise.all([this.coreIgnoredSettings, this.systemExtensionsIgnoredSettings, this.userExtensionsIgnoredSettings])).flat(); return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content); } + private async getIgnoredSettingForSystemExtensions(): Promise { + const systemExtensions = await this.extensionManagementService.getInstalled(ExtensionType.System); + return distinct(systemExtensions.map(e => getIgnoredSettingsForExtension(e.manifest)).flat()); + } + + private async getIgnoredSettingForUserExtensions(): Promise { + const userExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User, this.profile.extensionsResource); + return distinct(userExtensions.map(e => getIgnoredSettingsForExtension(e.manifest)).flat()); + } + private validateContent(content: string): void { if (this.hasErrors(content, false)) { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index b0776afe8dc..f6ebd219402 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -31,7 +31,7 @@ type AutoSyncClassification = { type AutoSyncErrorClassification = { owner: 'sandy081'; comment: 'Information about the error that causes auto sync to fail'; - code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'error code' }; + code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings sync service for which this error has occurred' }; }; diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index 5a532c8bae2..13b099ee13e 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -187,34 +187,26 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i if (localChange !== Change.None) { await this.backupLocal(stringifyLocalProfiles(this.getLocalUserDataProfiles(), false)); - const promises: Promise[] = []; - for (const profile of local.added) { - promises.push((async () => { - this.logService.trace(`${this.syncResourceLogLabel}: Creating '${profile.name}' profile...`); - await this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); - this.logService.info(`${this.syncResourceLogLabel}: Created profile '${profile.name}'.`); - })()); - } - for (const profile of local.removed) { - promises.push((async () => { - this.logService.trace(`${this.syncResourceLogLabel}: Removing '${profile.name}' profile...`); - await this.userDataProfilesService.removeProfile(profile); - this.logService.info(`${this.syncResourceLogLabel}: Removed profile '${profile.name}'.`); - })()); - } - for (const profile of local.updated) { + await Promise.all(local.removed.map(async profile => { + this.logService.trace(`${this.syncResourceLogLabel}: Removing '${profile.name}' profile...`); + await this.userDataProfilesService.removeProfile(profile); + this.logService.info(`${this.syncResourceLogLabel}: Removed profile '${profile.name}'.`); + })); + await Promise.all(local.added.map(async profile => { + this.logService.trace(`${this.syncResourceLogLabel}: Creating '${profile.name}' profile...`); + await this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); + this.logService.info(`${this.syncResourceLogLabel}: Created profile '${profile.name}'.`); + })); + await Promise.all(local.updated.map(async profile => { const localProfile = this.userDataProfilesService.profiles.find(p => p.id === profile.id); if (localProfile) { - promises.push((async () => { - this.logService.trace(`${this.syncResourceLogLabel}: Updating '${profile.name}' profile...`); - await this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); - this.logService.info(`${this.syncResourceLogLabel}: Updated profile '${profile.name}'.`); - })()); + this.logService.trace(`${this.syncResourceLogLabel}: Updating '${profile.name}' profile...`); + await this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); + this.logService.info(`${this.syncResourceLogLabel}: Updated profile '${profile.name}'.`); } else { this.logService.info(`${this.syncResourceLogLabel}: Could not find profile with id '${profile.id}' to update.`); } - } - await Promise.all(promises); + })); } if (remoteChange !== Change.None) { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 084755420cb..ed81c9196e8 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -15,9 +15,10 @@ import { isObject, isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IHeaders } from 'vs/base/parts/request/common/request'; import { localize } from 'vs/nls'; -import { allSettings, ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { allSettings, ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry, IRegisteredConfigurationPropertySchema, getAllConfigurationProperties, parseScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { EXTENSION_IDENTIFIER_PATTERN, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ILogService } from 'vs/platform/log/common/log'; @@ -30,12 +31,40 @@ export function getDisallowedIgnoredSettings(): string[] { return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore); } -export function getDefaultIgnoredSettings(): string[] { +export function getDefaultIgnoredSettings(excludeExtensions: boolean = false): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); - const ignoreSyncSettings = Object.keys(allSettings).filter(setting => !!allSettings[setting].ignoreSync); - const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); + const ignoredSettings = getIgnoredSettings(allSettings, excludeExtensions); const disallowedSettings = getDisallowedIgnoredSettings(); - return distinct([...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]); + return distinct([...ignoredSettings, ...disallowedSettings]); +} + +export function getIgnoredSettingsForExtension(manifest: IExtensionManifest): string[] { + if (!manifest.contributes?.configuration) { + return []; + } + const configurations = Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration]; + if (!configurations.length) { + return []; + } + const properties = getAllConfigurationProperties(configurations); + return getIgnoredSettings(properties, false); +} + +function getIgnoredSettings(properties: IStringDictionary, excludeExtensions: boolean): string[] { + const ignoredSettings = new Set(); + for (const key in properties) { + if (excludeExtensions && !!properties[key].source) { + continue; + } + const scope = isString(properties[key].scope) ? parseScope(properties[key].scope) : properties[key].scope; + if (properties[key].ignoreSync + || scope === ConfigurationScope.MACHINE + || scope === ConfigurationScope.MACHINE_OVERRIDABLE + ) { + ignoredSettings.add(key); + } + } + return [...ignoredSettings.values()]; } export const USER_DATA_SYNC_CONFIGURATION_SCOPE = 'settingsSync'; @@ -591,7 +620,7 @@ export interface IUserDataSyncUtilService { readonly _serviceBrand: undefined; resolveUserBindings(userbindings: string[]): Promise>; resolveFormattingOptions(resource: URI): Promise; - resolveDefaultIgnoredSettings(): Promise; + resolveDefaultCoreIgnoredSettings(): Promise; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts index 162d93328d2..e1606de959c 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts @@ -14,7 +14,7 @@ import { ALL_SYNC_RESOURCES, getEnablementKey, IUserDataSyncEnablementService, I type SyncEnablementClassification = { owner: 'sandy081'; comment: 'Reporting when Settings Sync is turned on or off'; - enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if settings sync is enabled or not' }; + enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if settings sync is enabled or not' }; }; const enablementKey = 'sync.enable'; diff --git a/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts index 28d2400cfee..8c505b3aec7 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts @@ -53,7 +53,7 @@ export class UserDataSyncLocalStoreService extends Disposable implements IUserDa if (stat.children) { for (const child of stat.children) { - if (child.isDirectory && !this.userDataProfilesService.profiles.some(profile => profile.id === child.name)) { + if (child.isDirectory && !ALL_SYNC_RESOURCES.includes(child.name) && !this.userDataProfilesService.profiles.some(profile => profile.id === child.name)) { try { this.logService.info('Deleting non existing profile from backup', child.resource.path); await this.fileService.del(child.resource, { recursive: true }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 928bff62414..a11d2cb04cc 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from 'vs/base/common/arrays'; -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; @@ -37,12 +37,12 @@ import { type SyncErrorClassification = { owner: 'sandy081'; comment: 'Information about the error that occurred while syncing'; - code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'error code' }; - service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Settings Sync service for which this error has occurred' }; - serverCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Settings Sync service error code' }; - url?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Settings Sync resource URL for which this error has occurred' }; - resource?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Settings Sync resource for which this error has occurred' }; - executionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Settings Sync execution id for which this error has occurred' }; + code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; + service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service for which this error has occurred' }; + serverCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service error code' }; + url?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource URL for which this error has occurred' }; + resource?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource for which this error has occurred' }; + executionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync execution id for which this error has occurred' }; }; const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime'; @@ -98,6 +98,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized; this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION, undefined); this._register(toDisposable(() => this.clearActiveProfileSynchronizers())); + + this._register(new RunOnceScheduler(() => this.cleanUpStaleStorageData(), 5 * 1000 /* after 5s */)).schedule(); } async createSyncTask(manifest: IUserDataManifest | null, disableCache?: boolean): Promise { @@ -245,6 +247,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (this.userDataProfilesService.profiles.some(p => p.id === profileSynchronizerItem[0].profile.id)) { continue; } + await profileSynchronizerItem[0].resetLocal(); profileSynchronizerItem[1].dispose(); this.activeProfileSynchronizers.delete(key); } @@ -397,6 +400,46 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.logService.info('Did reset the local sync state.'); } + private async cleanUpStaleStorageData(): Promise { + const allKeys = this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE); + const lastSyncProfileKeys: [string, string][] = []; + for (const key of allKeys) { + if (!key.endsWith('.lastSyncUserData')) { + continue; + } + const segments = key.split('.'); + if (segments.length === 3) { + lastSyncProfileKeys.push([key, segments[0]]); + } + } + if (!lastSyncProfileKeys.length) { + return; + } + + const disposables = new DisposableStore(); + + try { + let defaultProfileSynchronizer = this.activeProfileSynchronizers.get(this.userDataProfilesService.defaultProfile.id)?.[0]; + if (!defaultProfileSynchronizer) { + defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, this.userDataProfilesService.defaultProfile, undefined)); + } + const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles) as UserDataProfilesManifestSynchroniser; + if (!userDataProfileManifestSynchronizer) { + return; + } + const lastSyncedProfiles = await userDataProfileManifestSynchronizer.getLastSyncedProfiles(); + const lastSyncedCollections = lastSyncedProfiles?.map(p => p.collection) ?? []; + for (const [key, collection] of lastSyncProfileKeys) { + if (!lastSyncedCollections.includes(collection)) { + this.logService.info(`Removing last sync state for stale profile: ${collection}`); + this.storageService.remove(key, StorageScope.APPLICATION); + } + } + } finally { + disposables.dispose(); + } + } + async cleanUpRemoteData(): Promise { const remoteProfiles = await this.userDataSyncResourceProviderService.getRemoteSyncedProfiles(); const remoteProfileCollections = remoteProfiles.map(profile => profile.collection); @@ -659,8 +702,7 @@ class ProfileSynchronizer extends Disposable { const [[synchronizer, , disposable]] = this._enabled.splice(index, 1); disposable.dispose(); this.updateStatus(); - Promise.allSettled([synchronizer.stop(), synchronizer.resetLocal()]) - .then(null, error => this.logService.error(error)); + synchronizer.stop().then(null, error => this.logService.error(error)); } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 32c4086b550..491dad6ee96 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -148,7 +148,7 @@ export class UserDataSyncStoreClient extends Disposable { private userDataSyncStoreUrl: URI | undefined; private authToken: { token: string; type: string } | undefined; - private readonly commonHeadersPromise: Promise<{ [key: string]: string }>; + private readonly commonHeadersPromise: Promise; private readonly session: RequestsSession; private _onTokenFailed = this._register(new Emitter()); diff --git a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts index 3436a31fb07..1ecc7b900a4 100644 --- a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { ILocalSyncExtension, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; diff --git a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts index 3c61cd98aca..7a17fac286d 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index d27a2d8d6ac..7d3ecaca732 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index 935c553a872..c7a1f8675e7 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 4799201077a..660185fd8fd 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index 274ac5ee696..625df21215c 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { addSetting, merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import type { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 0cd110208b8..39246f25ce1 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts index f41039cbb9e..50e5caa545f 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index fc9644c2245..97f4ab83f13 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { IStringDictionary } from 'vs/base/common/collections'; import { dirname, joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index d226ceb9dba..a336ca32411 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -191,295 +191,327 @@ suite('TestSynchronizer - Auto Sync', () => { await client.setUp(); }); - test('status is syncing', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + test('status is syncing', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - const actual: SyncStatus[] = []; - disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - const promise = Event.toPromise(testObject.onDoSyncCall.event); + const promise = Event.toPromise(testObject.onDoSyncCall.event); - testObject.sync(await client.getResourceManifest()); - await promise; + testObject.sync(await client.getResourceManifest()); + await promise; - assert.deepStrictEqual(actual, [SyncStatus.Syncing]); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assert.deepStrictEqual(actual, [SyncStatus.Syncing]); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - testObject.stop(); - })); + testObject.stop(); + }); + }); - test('status is set correctly when sync is finished', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); + test('status is set correctly when sync is finished', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); - const actual: SyncStatus[] = []; - disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - })); - - test('status is set correctly when sync has errors', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasError: true, hasConflicts: false }; - testObject.syncBarrier.open(); - - const actual: SyncStatus[] = []; - disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - - try { + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); await testObject.sync(await client.getResourceManifest()); - assert.fail('Should fail'); - } catch (e) { + assert.deepStrictEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - } - })); - - test('status is set to hasConflicts when asked to sync if there are conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - assertConflicts(testObject.conflicts.conflicts, [testObject.localResource]); - })); - - test('sync should not run if syncing already', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - const promise = Event.toPromise(testObject.onDoSyncCall.event); - - testObject.sync(await client.getResourceManifest()); - await promise; - - const actual: SyncStatus[] = []; - disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(actual, []); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - - await testObject.stop(); - })); - - test('sync should not run if there are conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const actual: SyncStatus[] = []; - disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(actual, []); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - })); - - test('accept preview during conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].previewResource); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertConflicts(testObject.conflicts.conflicts, []); - - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const fileService = client.instantiationService.get(IFileService); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, (await fileService.readFile(testObject.localResource)).value.toString()); - })); - - test('accept remote during conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const fileService = client.instantiationService.get(IFileService); - const currentRemoteContent = (await testObject.getRemoteUserData(null)).syncData?.content; - const newLocalContent = 'conflict'; - await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].remoteResource); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertConflicts(testObject.conflicts.conflicts, []); - - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, currentRemoteContent); - assert.strictEqual((await fileService.readFile(testObject.localResource)).value.toString(), currentRemoteContent); - })); - - test('accept local during conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const fileService = client.instantiationService.get(IFileService); - const newLocalContent = 'conflict'; - await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].localResource); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertConflicts(testObject.conflicts.conflicts, []); - - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, newLocalContent); - assert.strictEqual((await fileService.readFile(testObject.localResource)).value.toString(), newLocalContent); - })); - - test('accept new content during conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const fileService = client.instantiationService.get(IFileService); - const newLocalContent = 'conflict'; - await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - const mergeContent = 'newContent'; - await testObject.accept(testObject.conflicts.conflicts[0].previewResource, mergeContent); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertConflicts(testObject.conflicts.conflicts, []); - - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, mergeContent); - assert.strictEqual((await fileService.readFile(testObject.localResource)).value.toString(), mergeContent); - })); - - test('accept delete during conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const fileService = client.instantiationService.get(IFileService); - const newLocalContent = 'conflict'; - await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].previewResource, null); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertConflicts(testObject.conflicts.conflicts, []); - - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, ''); - assert.ok(!(await fileService.exists(testObject.localResource))); - })); - - test('accept deleted local during conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const fileService = client.instantiationService.get(IFileService); - await fileService.del(testObject.localResource); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].localResource); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertConflicts(testObject.conflicts.conflicts, []); - - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, ''); - assert.ok(!(await fileService.exists(testObject.localResource))); - })); - - test('accept deleted remote during conflicts', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - const fileService = client.instantiationService.get(IFileService); - await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content')); - testObject.syncResult = { hasConflicts: true, hasError: false }; - - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].remoteResource); - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertConflicts(testObject.conflicts.conflicts, []); - - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData, null); - assert.ok(!(await fileService.exists(testObject.localResource))); - })); - - test('request latest data on precondition failure', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - // Sync once - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - testObject.syncBarrier = new Barrier(); - - // update remote data before syncing so that 412 is thrown by server - const disposable = testObject.onDoSyncCall.event(async () => { - disposable.dispose(); - await testObject.applyRef(ref, ref); - server.reset(); - testObject.syncBarrier.open(); }); + }); - // Start sycing - const manifest = await client.getResourceManifest(); - const ref = manifest![testObject.resource]; - await testObject.sync(await client.getResourceManifest()); + test('status is set correctly when sync has errors', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasError: true, hasConflicts: false }; + testObject.syncBarrier.open(); - assert.deepStrictEqual(server.requests, [ - { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': ref } }, - { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, - { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': `${parseInt(ref) + 1}` } }, - ]); - })); + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - test('no requests are made to server when local change is triggered', () => runWithFakedTimers({ useFakeTimers: true }, () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); + try { + await testObject.sync(await client.getResourceManifest()); + assert.fail('Should fail'); + } catch (e) { + assert.deepStrictEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + } + }); + }); - server.reset(); - const promise = Event.toPromise(testObject.onDidTriggerLocalChangeCall.event); - testObject.testTriggerLocalChange(); + test('status is set to hasConflicts when asked to sync if there are conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); - await promise; - assert.deepStrictEqual(server.requests, []); - }))); - - test('status is reset when getting latest remote data fails', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.failWhenGettingLatestRemoteUserData = true; - - try { await testObject.sync(await client.getResourceManifest()); - assert.fail('Should throw an error'); - } catch (error) { - } - assert.strictEqual(testObject.status, SyncStatus.Idle); - })); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + assertConflicts(testObject.conflicts.conflicts, [testObject.localResource]); + }); + }); + + test('sync should not run if syncing already', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + const promise = Event.toPromise(testObject.onDoSyncCall.event); + + testObject.sync(await client.getResourceManifest()); + await promise; + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(actual, []); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + + await testObject.stop(); + }); + }); + + test('sync should not run if there are conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(actual, []); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + }); + }); + + test('accept preview during conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].previewResource); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts.conflicts, []); + + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const fileService = client.instantiationService.get(IFileService); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, (await fileService.readFile(testObject.localResource)).value.toString()); + }); + }); + + test('accept remote during conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + const fileService = client.instantiationService.get(IFileService); + const currentRemoteContent = (await testObject.getRemoteUserData(null)).syncData?.content; + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].remoteResource); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts.conflicts, []); + + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, currentRemoteContent); + assert.strictEqual((await fileService.readFile(testObject.localResource)).value.toString(), currentRemoteContent); + }); + }); + + test('accept local during conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + const fileService = client.instantiationService.get(IFileService); + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].localResource); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts.conflicts, []); + + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, newLocalContent); + assert.strictEqual((await fileService.readFile(testObject.localResource)).value.toString(), newLocalContent); + }); + }); + + test('accept new content during conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + const fileService = client.instantiationService.get(IFileService); + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + const mergeContent = 'newContent'; + await testObject.accept(testObject.conflicts.conflicts[0].previewResource, mergeContent); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts.conflicts, []); + + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, mergeContent); + assert.strictEqual((await fileService.readFile(testObject.localResource)).value.toString(), mergeContent); + }); + }); + + test('accept delete during conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + const fileService = client.instantiationService.get(IFileService); + const newLocalContent = 'conflict'; + await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent)); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].previewResource, null); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts.conflicts, []); + + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, ''); + assert.ok(!(await fileService.exists(testObject.localResource))); + }); + }); + + test('accept deleted local during conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + const fileService = client.instantiationService.get(IFileService); + await fileService.del(testObject.localResource); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].localResource); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts.conflicts, []); + + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, ''); + assert.ok(!(await fileService.exists(testObject.localResource))); + }); + }); + + test('accept deleted remote during conflicts', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + const fileService = client.instantiationService.get(IFileService); + await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content')); + testObject.syncResult = { hasConflicts: true, hasError: false }; + + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].remoteResource); + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertConflicts(testObject.conflicts.conflicts, []); + + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData, null); + assert.ok(!(await fileService.exists(testObject.localResource))); + }); + }); + + test('request latest data on precondition failure', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + // Sync once + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + testObject.syncBarrier = new Barrier(); + + // update remote data before syncing so that 412 is thrown by server + const disposable = testObject.onDoSyncCall.event(async () => { + disposable.dispose(); + await testObject.applyRef(ref, ref); + server.reset(); + testObject.syncBarrier.open(); + }); + + // Start sycing + const manifest = await client.getResourceManifest(); + const ref = manifest![testObject.resource]; + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': ref } }, + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': `${parseInt(ref) + 1}` } }, + ]); + }); + }); + + test('no requests are made to server when local change is triggered', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + server.reset(); + const promise = Event.toPromise(testObject.onDidTriggerLocalChangeCall.event); + testObject.testTriggerLocalChange(); + + await promise; + assert.deepStrictEqual(server.requests, []); + }); + }); + + test('status is reset when getting latest remote data fails', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.failWhenGettingLatestRemoteUserData = true; + + try { + await testObject.sync(await client.getResourceManifest()); + assert.fail('Should throw an error'); + } catch (error) { + } + + assert.strictEqual(testObject.status, SyncStatus.Idle); + }); + }); }); suite('TestSynchronizer - Manual Sync', () => { @@ -498,566 +530,632 @@ suite('TestSynchronizer - Manual Sync', () => { await client.setUp(); }); - test('preview', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - const preview = await testObject.preview(await client.getResourceManifest(), {}); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preview -> merge', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preview -> accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preview -> merge -> accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preview -> merge -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const manifest = await client.getResourceManifest(); - let preview = await testObject.preview(manifest, {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - const expectedContent = manifest![testObject.resource]; - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('preview -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const manifest = await client.getResourceManifest(); - const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('preview -> merge -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('preivew -> merge -> discard', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preivew -> merge -> discard -> accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preivew -> accept -> discard', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preivew -> accept -> discard -> accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preivew -> accept -> discard -> merge', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preivew -> merge -> accept -> discard', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('preivew -> merge -> discard -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('preivew -> accept -> discard -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('preivew -> accept -> discard -> merge -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const manifest = await client.getResourceManifest(); - const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('conflicts: preview', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - const preview = await testObject.preview(await client.getResourceManifest(), {}); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preview -> merge', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); - assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); - })); - - test('conflicts: preview -> merge -> discard', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - const preview = await testObject.preview(await client.getResourceManifest(), {}); - await testObject.merge(preview!.resourcePreviews[0].previewResource); - await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preview -> accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - await testObject.merge(preview!.resourcePreviews[0].previewResource); - const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preview -> merge -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - const manifest = await client.getResourceManifest(); - const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); - - await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('conflicts: preview -> accept 2', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preview -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - const manifest = await client.getResourceManifest(); - const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); - - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('conflicts: preivew -> merge -> discard', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preivew -> merge -> discard -> accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preivew -> accept -> discard', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preivew -> accept -> discard -> accept', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preivew -> accept -> discard -> merge', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); - assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); - })); - - test('conflicts: preivew -> merge -> discard -> merge', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); - assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); - })); - - test('conflicts: preivew -> merge -> accept -> discard', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - })); - - test('conflicts: preivew -> merge -> discard -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('conflicts: preivew -> accept -> discard -> accept -> apply', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); - - test('remote is accepted if last sync state does not exists in server', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - - await testObject.sync(await client.getResourceManifest()); - - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(); - const synchronizer2: TestSynchroniser = disposableStore.add(client2.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client2.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - synchronizer2.syncBarrier.open(); - const manifest = await client2.getResourceManifest(); - const expectedContent = manifest![testObject.resource]; - await synchronizer2.sync(manifest); - - await fileService.del(testObject.getLastSyncResource()); - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - })); + test('preview', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + const preview = await testObject.preview(await client.getResourceManifest(), {}); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preview -> merge', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preview -> accept', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preview -> merge -> accept', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preview -> merge -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const manifest = await client.getResourceManifest(); + let preview = await testObject.preview(manifest, {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + + const expectedContent = manifest![testObject.resource]; + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('preview -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const manifest = await client.getResourceManifest(); + const expectedContent = manifest![testObject.resource]; + let preview = await testObject.preview(manifest, {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('preview -> merge -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('preivew -> merge -> discard', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preivew -> merge -> discard -> accept', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preivew -> accept -> discard', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preivew -> accept -> discard -> accept', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preivew -> accept -> discard -> merge', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preivew -> merge -> accept -> discard', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('preivew -> merge -> discard -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('preivew -> accept -> discard -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('preivew -> accept -> discard -> merge -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const manifest = await client.getResourceManifest(); + const expectedContent = manifest![testObject.resource]; + let preview = await testObject.preview(manifest, {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('conflicts: preview', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + const preview = await testObject.preview(await client.getResourceManifest(), {}); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preview -> merge', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); + assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); + }); + }); + + test('conflicts: preview -> merge -> discard', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + const preview = await testObject.preview(await client.getResourceManifest(), {}); + await testObject.merge(preview!.resourcePreviews[0].previewResource); + await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preview -> accept', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + await testObject.merge(preview!.resourcePreviews[0].previewResource); + const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preview -> merge -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + const manifest = await client.getResourceManifest(); + const expectedContent = manifest![testObject.resource]; + let preview = await testObject.preview(manifest, {}); + + await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('conflicts: preview -> accept 2', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preview -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + testObject.syncResult = { hasConflicts: true, hasError: false }; + const manifest = await client.getResourceManifest(); + const expectedContent = manifest![testObject.resource]; + let preview = await testObject.preview(manifest, {}); + + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('conflicts: preivew -> merge -> discard', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preivew -> merge -> discard -> accept', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preivew -> accept -> discard', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preivew -> accept -> discard -> accept', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preivew -> accept -> discard -> merge', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); + assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); + }); + }); + + test('conflicts: preivew -> merge -> discard -> merge', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: true, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); + assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); + }); + }); + + test('conflicts: preivew -> merge -> accept -> discard', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + + assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); + assertPreviews(preview!.resourcePreviews, [testObject.localResource]); + assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); + assertConflicts(testObject.conflicts.conflicts, []); + }); + }); + + test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('conflicts: preivew -> accept -> discard -> accept -> apply', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncResult = { hasConflicts: false, hasError: false }; + testObject.syncBarrier.open(); + await testObject.sync(await client.getResourceManifest()); + + const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); + let preview = await testObject.preview(await client.getResourceManifest(), {}); + preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); + preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); + preview = await testObject.accept(preview!.resourcePreviews[0].localResource); + preview = await testObject.apply(false); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual(preview, null); + assertConflicts(testObject.conflicts.conflicts, []); + assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); + + test('remote is accepted if last sync state does not exists in server', async () => { + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + + await testObject.sync(await client.getResourceManifest()); + + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(); + const synchronizer2: TestSynchroniser = disposableStore.add(client2.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client2.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + synchronizer2.syncBarrier.open(); + const manifest = await client2.getResourceManifest(); + const expectedContent = manifest![testObject.resource]; + await synchronizer2.sync(manifest); + + await fileService.del(testObject.getLastSyncResource()); + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); + }); + }); }); @@ -1076,234 +1174,254 @@ suite('TestSynchronizer - Last Sync Data', () => { await client.setUp(); }); - test('last sync data is null when not synced before', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + test('last sync data is null when not synced before', async () => { + await runWithFakedTimers({}, async () => { + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - const actual = await testObject.getLastSyncUserData(); + const actual = await testObject.getLastSyncUserData(); - assert.strictEqual(actual, null); - })); - - test('last sync data is set after sync', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const storageService = client.instantiationService.get(IStorageService); - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - - await testObject.sync(await client.getResourceManifest()); - const machineId = await testObject.getMachineId(); - const actual = await testObject.getLastSyncUserData(); - - assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); - assert.deepStrictEqual(JSON.parse((await fileService.readFile(testObject.getLastSyncResource())).value.toString()), { ref: '1', syncData: { version: 1, machineId, content: '0' } }); - assert.deepStrictEqual(actual, { - ref: '1', - syncData: { - content: '0', - machineId, - version: 1 - }, + assert.strictEqual(actual, null); }); - })); + }); - test('last sync data is read from server after sync if last sync resource is deleted', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const storageService = client.instantiationService.get(IStorageService); - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); + test('last sync data is set after sync', async () => { + await runWithFakedTimers({}, async () => { + const storageService = client.instantiationService.get(IStorageService); + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const machineId = await testObject.getMachineId(); - await fileService.del(testObject.getLastSyncResource()); - const actual = await testObject.getLastSyncUserData(); + await testObject.sync(await client.getResourceManifest()); + const machineId = await testObject.getMachineId(); + const actual = await testObject.getLastSyncUserData(); - assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); - assert.deepStrictEqual(actual, { - ref: '1', - syncData: { - content: '0', - machineId, - version: 1 - }, + assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); + assert.deepStrictEqual(JSON.parse((await fileService.readFile(testObject.getLastSyncResource())).value.toString()), { ref: '1', syncData: { version: 1, machineId, content: '0' } }); + assert.deepStrictEqual(actual, { + ref: '1', + syncData: { + content: '0', + machineId, + version: 1 + }, + }); }); - })); + }); - test('last sync data is read from server after sync and sync data is invalid', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const storageService = client.instantiationService.get(IStorageService); - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); + test('last sync data is read from server after sync if last sync resource is deleted', async () => { + await runWithFakedTimers({}, async () => { + const storageService = client.instantiationService.get(IStorageService); + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const machineId = await testObject.getMachineId(); - await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ - ref: '1', - version: 1, - content: JSON.stringify({ - content: '0', - machineId, - version: 1 - }), - additionalData: { - foo: 'bar' - } - }))); - server.reset(); - const actual = await testObject.getLastSyncUserData(); + await testObject.sync(await client.getResourceManifest()); + const machineId = await testObject.getMachineId(); + await fileService.del(testObject.getLastSyncResource()); + const actual = await testObject.getLastSyncUserData(); - assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); - assert.deepStrictEqual(actual, { - ref: '1', - syncData: { - content: '0', - machineId, - version: 1 - }, + assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); + assert.deepStrictEqual(actual, { + ref: '1', + syncData: { + content: '0', + machineId, + version: 1 + }, + }); }); - assert.deepStrictEqual(server.requests, [{ headers: {}, type: 'GET', url: 'http://host:3000/v1/resource/settings/1' }]); - })); + }); - test('last sync data is read from server after sync and stored sync data is tampered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const storageService = client.instantiationService.get(IStorageService); - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); + test('last sync data is read from server after sync and sync data is invalid', async () => { + await runWithFakedTimers({}, async () => { + const storageService = client.instantiationService.get(IStorageService); + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const machineId = await testObject.getMachineId(); - await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ - ref: '2', - syncData: { - content: '0', - machineId, - version: 1 - } - }))); - server.reset(); - const actual = await testObject.getLastSyncUserData(); + await testObject.sync(await client.getResourceManifest()); + const machineId = await testObject.getMachineId(); + await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ + ref: '1', + version: 1, + content: JSON.stringify({ + content: '0', + machineId, + version: 1 + }), + additionalData: { + foo: 'bar' + } + }))); + server.reset(); + const actual = await testObject.getLastSyncUserData(); - assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); - assert.deepStrictEqual(actual, { - ref: '1', - syncData: { - content: '0', - machineId, - version: 1 - } + assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); + assert.deepStrictEqual(actual, { + ref: '1', + syncData: { + content: '0', + machineId, + version: 1 + }, + }); + assert.deepStrictEqual(server.requests, [{ headers: {}, type: 'GET', url: 'http://host:3000/v1/resource/settings/1' }]); }); - assert.deepStrictEqual(server.requests, [{ headers: {}, type: 'GET', url: 'http://host:3000/v1/resource/settings/1' }]); - })); + }); - test('reading last sync data: no requests are made to server when sync data is invalid', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); + test('last sync data is read from server after sync and stored sync data is tampered', async () => { + await runWithFakedTimers({}, async () => { + const storageService = client.instantiationService.get(IStorageService); + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const machineId = await testObject.getMachineId(); - await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ - ref: '1', - version: 1, - content: JSON.stringify({ - content: '0', - machineId, - version: 1 - }), - additionalData: { - foo: 'bar' - } - }))); - await testObject.getLastSyncUserData(); - server.reset(); + await testObject.sync(await client.getResourceManifest()); + const machineId = await testObject.getMachineId(); + await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ + ref: '2', + syncData: { + content: '0', + machineId, + version: 1 + } + }))); + server.reset(); + const actual = await testObject.getLastSyncUserData(); - await testObject.getLastSyncUserData(); - assert.deepStrictEqual(server.requests, []); - })); - - test('reading last sync data: no requests are made to server when sync data is null', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - - await testObject.sync(await client.getResourceManifest()); - server.reset(); - await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ - ref: '1', - syncData: null, - }))); - await testObject.getLastSyncUserData(); - - assert.deepStrictEqual(server.requests, []); - })); - - test('last sync data is null after sync if last sync state is deleted', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const storageService = client.instantiationService.get(IStorageService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - - await testObject.sync(await client.getResourceManifest()); - storageService.remove('settings.lastSyncUserData', StorageScope.APPLICATION); - const actual = await testObject.getLastSyncUserData(); - - assert.strictEqual(actual, null); - })); - - test('last sync data is null after sync if last sync content is deleted everywhere', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const storageService = client.instantiationService.get(IStorageService); - const fileService = client.instantiationService.get(IFileService); - const userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncBarrier.open(); - - await testObject.sync(await client.getResourceManifest()); - await fileService.del(testObject.getLastSyncResource()); - await userDataSyncStoreService.deleteResource(testObject.syncResource.syncResource, null); - const actual = await testObject.getLastSyncUserData(); - - assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); - assert.strictEqual(actual, null); - })); - - test('last sync data is migrated', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - const storageService = client.instantiationService.get(IStorageService); - const fileService = client.instantiationService.get(IFileService); - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - const machineId = await testObject.getMachineId(); - await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ - ref: '1', - version: 1, - content: JSON.stringify({ - content: '0', - machineId, - version: 1 - }), - additionalData: { - foo: 'bar' - } - }))); - - const actual = await testObject.getLastSyncUserData(); - - assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ - ref: '1', - version: 1, - additionalData: { - foo: 'bar' - } - })); - assert.deepStrictEqual(actual, { - ref: '1', - version: 1, - syncData: { - content: '0', - machineId, - version: 1 - }, - additionalData: { - foo: 'bar' - } + assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); + assert.deepStrictEqual(actual, { + ref: '1', + syncData: { + content: '0', + machineId, + version: 1 + } + }); + assert.deepStrictEqual(server.requests, [{ headers: {}, type: 'GET', url: 'http://host:3000/v1/resource/settings/1' }]); }); - })); + }); + + test('reading last sync data: no requests are made to server when sync data is invalid', async () => { + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + + await testObject.sync(await client.getResourceManifest()); + const machineId = await testObject.getMachineId(); + await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ + ref: '1', + version: 1, + content: JSON.stringify({ + content: '0', + machineId, + version: 1 + }), + additionalData: { + foo: 'bar' + } + }))); + await testObject.getLastSyncUserData(); + server.reset(); + + await testObject.getLastSyncUserData(); + assert.deepStrictEqual(server.requests, []); + }); + }); + + test('reading last sync data: no requests are made to server when sync data is null', async () => { + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + + await testObject.sync(await client.getResourceManifest()); + server.reset(); + await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ + ref: '1', + syncData: null, + }))); + await testObject.getLastSyncUserData(); + + assert.deepStrictEqual(server.requests, []); + }); + }); + + test('last sync data is null after sync if last sync state is deleted', async () => { + await runWithFakedTimers({}, async () => { + const storageService = client.instantiationService.get(IStorageService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + + await testObject.sync(await client.getResourceManifest()); + storageService.remove('settings.lastSyncUserData', StorageScope.APPLICATION); + const actual = await testObject.getLastSyncUserData(); + + assert.strictEqual(actual, null); + }); + }); + + test('last sync data is null after sync if last sync content is deleted everywhere', async () => { + await runWithFakedTimers({}, async () => { + const storageService = client.instantiationService.get(IStorageService); + const fileService = client.instantiationService.get(IFileService); + const userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + testObject.syncBarrier.open(); + + await testObject.sync(await client.getResourceManifest()); + await fileService.del(testObject.getLastSyncResource()); + await userDataSyncStoreService.deleteResource(testObject.syncResource.syncResource, null); + const actual = await testObject.getLastSyncUserData(); + + assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' })); + assert.strictEqual(actual, null); + }); + }); + + test('last sync data is migrated', async () => { + await runWithFakedTimers({}, async () => { + const storageService = client.instantiationService.get(IStorageService); + const fileService = client.instantiationService.get(IFileService); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); + const machineId = await testObject.getMachineId(); + await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({ + ref: '1', + version: 1, + content: JSON.stringify({ + content: '0', + machineId, + version: 1 + }), + additionalData: { + foo: 'bar' + } + }))); + + const actual = await testObject.getLastSyncUserData(); + + assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ + ref: '1', + version: 1, + additionalData: { + foo: 'bar' + } + })); + assert.deepStrictEqual(actual, { + ref: '1', + version: 1, + syncData: { + content: '0', + machineId, + version: 1 + }, + additionalData: { + foo: 'bar' + } + }); + }); + }); }); function assertConflicts(actual: IBaseResourcePreview[], expected: URI[]) { diff --git a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts index bfb44062bcd..c6cfd18a44b 100644 --- a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; @@ -33,514 +34,544 @@ suite('TasksSync', () => { }); test('when tasks file does not exist', async () => { - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - assert.deepStrictEqual(await testObject.getLastSyncUserData(), null); - let manifest = await client.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); + assert.deepStrictEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); - assert.deepStrictEqual(server.requests, [ - { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, - ]); - assert.ok(!await fileService.exists(tasksResource)); + assert.deepStrictEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(tasksResource)); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); - assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.strictEqual(lastSyncUserData!.syncData, null); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.strictEqual(lastSyncUserData!.syncData, null); - manifest = await client.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); - assert.deepStrictEqual(server.requests, []); + manifest = await client.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepStrictEqual(server.requests, []); - manifest = await client.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); - assert.deepStrictEqual(server.requests, []); + manifest = await client.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepStrictEqual(server.requests, []); + }); }); test('when tasks file does not exist and remote has changes', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + await client2.instantiationService.get(IFileService).writeFile(tasksResource2, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.instantiationService.get(IFileService).writeFile(tasksResource2, VSBuffer.fromString(content)); - await client2.sync(); - - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); test('when tasks file exists locally and remote has no tasks', async () => { - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); }); - fileService.writeFile(tasksResource, VSBuffer.fromString(content)); - - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); }); test('first time sync: when tasks file exists locally with same content as remote', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + await client2.instantiationService.get(IFileService).writeFile(tasksResource2, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + await fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.instantiationService.get(IFileService).writeFile(tasksResource2, VSBuffer.fromString(content)); - await client2.sync(); - - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await fileService.writeFile(tasksResource, VSBuffer.fromString(content)); - - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); test('when tasks file locally has moved forward', async () => { - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); - await testObject.sync(await client.getResourceManifest()); + await testObject.sync(await client.getResourceManifest()); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); }); - fileService.writeFile(tasksResource, VSBuffer.fromString(content)); - - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); }); test('when tasks file remotely has moved forward', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const fileService2 = client2.instantiationService.get(IFileService); - await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.sync(); - await testObject.sync(await client.getResourceManifest()); + await client2.sync(); + await testObject.sync(await client.getResourceManifest()); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); + + await client2.sync(); + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); - fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); - - await client2.sync(); - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); test('when tasks file has moved forward locally and remotely with same changes', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const fileService2 = client2.instantiationService.get(IFileService); - await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.sync(); - await testObject.sync(await client.getResourceManifest()); + await client2.sync(); + await testObject.sync(await client.getResourceManifest()); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); + await client2.sync(); + + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + await testObject.sync(await client.getResourceManifest()); + + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); - fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); - await client2.sync(); - - fileService.writeFile(tasksResource, VSBuffer.fromString(content)); - await testObject.sync(await client.getResourceManifest()); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); test('when tasks file has moved forward locally and remotely - accept preview', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const fileService2 = client2.instantiationService.get(IFileService); - await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.sync(); - await testObject.sync(await client.getResourceManifest()); + await client2.sync(); + await testObject.sync(await client.getResourceManifest()); - fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - }] - }))); - await client2.sync(); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }))); + await client2.sync(); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + await testObject.sync(await client.getResourceManifest()); + + const previewContent = (await fileService.readFile(testObject.conflicts.conflicts[0].previewResource)).value.toString(); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + assert.deepStrictEqual(testObject.conflicts.conflicts.length, 1); + assert.deepStrictEqual(testObject.conflicts.conflicts[0].mergeState, MergeState.Conflict); + assert.deepStrictEqual(testObject.conflicts.conflicts[0].localChange, Change.Modified); + assert.deepStrictEqual(testObject.conflicts.conflicts[0].remoteChange, Change.Modified); + + await testObject.accept(testObject.conflicts.conflicts[0].previewResource); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), previewContent); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), previewContent); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), previewContent); }); - fileService.writeFile(tasksResource, VSBuffer.fromString(content)); - await testObject.sync(await client.getResourceManifest()); - - const previewContent = (await fileService.readFile(testObject.conflicts.conflicts[0].previewResource)).value.toString(); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - assert.deepStrictEqual(testObject.conflicts.conflicts.length, 1); - assert.deepStrictEqual(testObject.conflicts.conflicts[0].mergeState, MergeState.Conflict); - assert.deepStrictEqual(testObject.conflicts.conflicts[0].localChange, Change.Modified); - assert.deepStrictEqual(testObject.conflicts.conflicts[0].remoteChange, Change.Modified); - - await testObject.accept(testObject.conflicts.conflicts[0].previewResource); - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), previewContent); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), previewContent); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), previewContent); }); test('when tasks file has moved forward locally and remotely - accept modified preview', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const fileService2 = client2.instantiationService.get(IFileService); - await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.sync(); - await testObject.sync(await client.getResourceManifest()); + await client2.sync(); + await testObject.sync(await client.getResourceManifest()); - fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - }] - }))); - await client2.sync(); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }))); + await client2.sync(); - fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] - }))); - await testObject.sync(await client.getResourceManifest()); + fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }))); + await testObject.sync(await client.getResourceManifest()); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch 2' - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch 2' + }] + }); + await testObject.accept(testObject.conflicts.conflicts[0].previewResource, content); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); - await testObject.accept(testObject.conflicts.conflicts[0].previewResource, content); - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); test('when tasks file has moved forward locally and remotely - accept remote', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const fileService2 = client2.instantiationService.get(IFileService); - await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.sync(); - await testObject.sync(await client.getResourceManifest()); + await client2.sync(); + await testObject.sync(await client.getResourceManifest()); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); + await client2.sync(); + + fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }))); + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].remoteResource); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); - fileService2.writeFile(tasksResource2, VSBuffer.fromString(content)); - await client2.sync(); - - fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] - }))); - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].remoteResource); - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); test('when tasks file has moved forward locally and remotely - accept local', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const fileService2 = client2.instantiationService.get(IFileService); - await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService2 = client2.instantiationService.get(IFileService); + await fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await client2.sync(); - await testObject.sync(await client.getResourceManifest()); + await client2.sync(); + await testObject.sync(await client.getResourceManifest()); - fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - }] - }))); - await client2.sync(); + fileService2.writeFile(tasksResource2, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + }] + }))); + await client2.sync(); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + fileService.writeFile(tasksResource, VSBuffer.fromString(content)); + await testObject.sync(await client.getResourceManifest()); + assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); + + await testObject.accept(testObject.conflicts.conflicts[0].localResource); + await testObject.apply(false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); + assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); - fileService.writeFile(tasksResource, VSBuffer.fromString(content)); - await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - - await testObject.accept(testObject.conflicts.conflicts[0].localResource); - await testObject.apply(false); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), content); - assert.strictEqual((await fileService.readFile(tasksResource)).value.toString(), content); }); test('when tasks file was removed in one client', async () => { - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ - 'version': '2.0.0', - 'tasks': [] - }))); - await testObject.sync(await client.getResourceManifest()); + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + await fileService.writeFile(tasksResource, VSBuffer.fromString(JSON.stringify({ + 'version': '2.0.0', + 'tasks': [] + }))); + await testObject.sync(await client.getResourceManifest()); - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - await client2.sync(); + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + await client2.sync(); - const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - const fileService2 = client2.instantiationService.get(IFileService); - fileService2.del(tasksResource2); - await client2.sync(); + const tasksResource2 = client2.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + const fileService2 = client2.instantiationService.get(IFileService); + fileService2.del(tasksResource2); + await client2.sync(); - await testObject.sync(await client.getResourceManifest()); + await testObject.sync(await client.getResourceManifest()); - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), null); - assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), null); - assert.strictEqual(await fileService.exists(tasksResource), false); + assert.deepStrictEqual(testObject.status, SyncStatus.Idle); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), null); + assert.strictEqual(getTasksContentFromSyncContent(remoteUserData.syncData!.content, client.instantiationService.get(ILogService)), null); + assert.strictEqual(await fileService.exists(tasksResource), false); + }); }); test('when tasks file is created after first sync', async () => { - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - await testObject.sync(await client.getResourceManifest()); + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + await testObject.sync(await client.getResourceManifest()); - const content = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + const content = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + await fileService.createFile(tasksResource, VSBuffer.fromString(content)); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepStrictEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); }); - await fileService.createFile(tasksResource, VSBuffer.fromString(content)); - - let lastSyncUserData = await testObject.getLastSyncUserData(); - const manifest = await client.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); - - assert.deepStrictEqual(server.requests, [ - { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, - ]); - - lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); - assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.strictEqual(getTasksContentFromSyncContent(lastSyncUserData!.syncData!.content, client.instantiationService.get(ILogService)), content); }); test('apply remote when tasks file does not exist', async () => { - const fileService = client.instantiationService.get(IFileService); - const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; - if (await fileService.exists(tasksResource)) { - await fileService.del(tasksResource); - } + await runWithFakedTimers({}, async () => { + const fileService = client.instantiationService.get(IFileService); + const tasksResource = client.instantiationService.get(IUserDataProfilesService).defaultProfile.tasksResource; + if (await fileService.exists(tasksResource)) { + await fileService.del(tasksResource); + } - const preview = (await testObject.preview(await client.getResourceManifest(), {}))!; + const preview = (await testObject.preview(await client.getResourceManifest(), {}))!; - server.reset(); - const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource); - await testObject.accept(preview.resourcePreviews[0].remoteResource, content); - await testObject.apply(false); - assert.deepStrictEqual(server.requests, []); + server.reset(); + const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource); + await testObject.accept(preview.resourcePreviews[0].remoteResource, content); + await testObject.apply(false); + assert.deepStrictEqual(server.requests, []); + }); }); test('sync profile tasks', async () => { - const client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(true); - const profile = await client2.instantiationService.get(IUserDataProfilesService).createNamedProfile('profile1'); - const expected = JSON.stringify({ - 'version': '2.0.0', - 'tasks': [{ - 'type': 'npm', - 'script': 'watch', - 'label': 'Watch' - }] + await runWithFakedTimers({}, async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const profile = await client2.instantiationService.get(IUserDataProfilesService).createNamedProfile('profile1'); + const expected = JSON.stringify({ + 'version': '2.0.0', + 'tasks': [{ + 'type': 'npm', + 'script': 'watch', + 'label': 'Watch' + }] + }); + await client2.instantiationService.get(IFileService).createFile(profile.tasksResource, VSBuffer.fromString(expected)); + await client2.sync(); + + await client.sync(); + + const syncedProfile = client.instantiationService.get(IUserDataProfilesService).profiles.find(p => p.id === profile.id)!; + const actual = (await client.instantiationService.get(IFileService).readFile(syncedProfile.tasksResource)).value.toString(); + assert.strictEqual(actual, expected); }); - await client2.instantiationService.get(IFileService).createFile(profile.tasksResource, VSBuffer.fromString(expected)); - await client2.sync(); - - await client.sync(); - - const syncedProfile = client.instantiationService.get(IUserDataProfilesService).profiles.find(p => p.id === profile.id)!; - const actual = (await client.instantiationService.get(IFileService).readFile(syncedProfile.tasksResource)).value.toString(); - assert.strictEqual(actual, expected); }); }); diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 47cde8003c7..e9c86afd301 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts index 40cc6f3623d..e60f5314799 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts index 87c687f10c1..2227e3d1182 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesManifestSynchroniser } from 'vs/platform/userDataSync/common/userDataProfilesManifestSync'; @@ -34,230 +35,254 @@ suite('UserDataProfilesManifestSync', () => { }); test('when profiles does not exist', async () => { - assert.deepStrictEqual(await testObject.getLastSyncUserData(), null); - let manifest = await testClient.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); + await runWithFakedTimers({}, async () => { + assert.deepStrictEqual(await testObject.getLastSyncUserData(), null); + let manifest = await testClient.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); - assert.deepStrictEqual(server.requests, [ - { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, - ]); + assert.deepStrictEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); - const lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); - assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.strictEqual(lastSyncUserData!.syncData, null); + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.strictEqual(lastSyncUserData!.syncData, null); - manifest = await testClient.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); - assert.deepStrictEqual(server.requests, []); + manifest = await testClient.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepStrictEqual(server.requests, []); - manifest = await testClient.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); - assert.deepStrictEqual(server.requests, []); + manifest = await testClient.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepStrictEqual(server.requests, []); + }); }); test('when profile is created after first sync', async () => { - await testObject.sync(await testClient.getResourceManifest()); - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', '1'); + await runWithFakedTimers({}, async () => { + await testObject.sync(await testClient.getResourceManifest()); + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', '1'); - let lastSyncUserData = await testObject.getLastSyncUserData(); - const manifest = await testClient.getResourceManifest(); - server.reset(); - await testObject.sync(manifest); + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await testClient.getResourceManifest(); + server.reset(); + await testObject.sync(manifest); - assert.deepStrictEqual(server.requests, [ - { type: 'POST', url: `${server.url}/v1/collection`, headers: {} }, - { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, - ]); + assert.deepStrictEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/collection`, headers: {} }, + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); - lastSyncUserData = await testObject.getLastSyncUserData(); - const remoteUserData = await testObject.getRemoteUserData(null); - assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); - assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.deepStrictEqual(JSON.parse(lastSyncUserData!.syncData!.content), [{ 'name': '1', 'id': '1', 'collection': '1' }]); + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.deepStrictEqual(JSON.parse(lastSyncUserData!.syncData!.content), [{ 'name': '1', 'id': '1', 'collection': '1' }]); + }); }); test('first time sync - outgoing to server (no state)', async () => { - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', '1'); + await runWithFakedTimers({}, async () => { + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', '1'); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - assert.deepStrictEqual(JSON.parse(JSON.parse(content).content), [{ 'name': '1', 'id': '1', 'collection': '1' }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + assert.deepStrictEqual(JSON.parse(JSON.parse(content).content), [{ 'name': '1', 'id': '1', 'collection': '1' }]); + }); }); test('first time sync - incoming from server (no state)', async () => { - await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); - await client2.sync(); + await runWithFakedTimers({}, async () => { + await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await client2.sync(); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); - const profiles = getLocalProfiles(testClient); - assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: undefined }]); + const profiles = getLocalProfiles(testClient); + assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: undefined }]); + }); }); test('first time sync when profiles exists', async () => { - await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); - await client2.sync(); + await runWithFakedTimers({}, async () => { + await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await client2.sync(); - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('2', 'name 2'); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('2', 'name 2'); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); - const profiles = getLocalProfiles(testClient); - assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: undefined }, { id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); + const profiles = getLocalProfiles(testClient); + assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: undefined }, { id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1' }, { id: '2', name: 'name 2', collection: '2' }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1' }, { id: '2', name: 'name 2', collection: '2' }]); + }); }); test('first time sync when storage exists - has conflicts', async () => { - await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); - await client2.sync(); + await runWithFakedTimers({}, async () => { + await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await client2.sync(); - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 2'); - await testObject.sync(await testClient.getResourceManifest()); + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 2'); + await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); - const profiles = getLocalProfiles(testClient); - assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: undefined }]); + const profiles = getLocalProfiles(testClient); + assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: undefined }]); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1' }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1' }]); + }); }); test('sync adding a profile', async () => { - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1', { shortName: 'short 1' }); - await testObject.sync(await testClient.getResourceManifest()); - await client2.sync(); + await runWithFakedTimers({}, async () => { + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1', { shortName: 'short 1' }); + await testObject.sync(await testClient.getResourceManifest()); + await client2.sync(); - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('2', 'name 2'); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: 'short 1', useDefaultFlags: undefined }, { id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('2', 'name 2'); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: 'short 1', useDefaultFlags: undefined }, { id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); - await client2.sync(); - assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 1', shortName: 'short 1', useDefaultFlags: undefined }, { id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); + await client2.sync(); + assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 1', shortName: 'short 1', useDefaultFlags: undefined }, { id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', shortName: 'short 1' }, { id: '2', name: 'name 2', collection: '2' }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', shortName: 'short 1' }, { id: '2', name: 'name 2', collection: '2' }]); + }); }); test('sync updating a profile', async () => { - const profile = await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); - await testObject.sync(await testClient.getResourceManifest()); - await client2.sync(); + await runWithFakedTimers({}, async () => { + const profile = await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await testObject.sync(await testClient.getResourceManifest()); + await client2.sync(); - await testClient.instantiationService.get(IUserDataProfilesService).updateProfile(profile, { name: 'name 2', shortName: '2' }); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 2', shortName: '2', useDefaultFlags: undefined }]); + await testClient.instantiationService.get(IUserDataProfilesService).updateProfile(profile, { name: 'name 2', shortName: '2' }); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 2', shortName: '2', useDefaultFlags: undefined }]); - await client2.sync(); - assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 2', shortName: '2', useDefaultFlags: undefined }]); + await client2.sync(); + assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 2', shortName: '2', useDefaultFlags: undefined }]); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 2', collection: '1', shortName: '2' }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 2', collection: '1', shortName: '2' }]); + }); }); test('sync removing a profile', async () => { - const profile = await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('2', 'name 2'); - await testObject.sync(await testClient.getResourceManifest()); - await client2.sync(); + await runWithFakedTimers({}, async () => { + const profile = await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('2', 'name 2'); + await testObject.sync(await testClient.getResourceManifest()); + await client2.sync(); - testClient.instantiationService.get(IUserDataProfilesService).removeProfile(profile); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); + testClient.instantiationService.get(IUserDataProfilesService).removeProfile(profile); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); - await client2.sync(); - assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); + await client2.sync(); + assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '2', name: 'name 2', shortName: undefined, useDefaultFlags: undefined }]); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '2', name: 'name 2', collection: '2' }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '2', name: 'name 2', collection: '2' }]); + }); }); test('sync profile that uses default profile', async () => { - await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1', { useDefaultFlags: { keybindings: true } }); - await client2.sync(); + await runWithFakedTimers({}, async () => { + await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1', { useDefaultFlags: { keybindings: true } }); + await client2.sync(); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', useDefaultFlags: { keybindings: true } }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', useDefaultFlags: { keybindings: true } }]); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: { keybindings: true } }]); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: { keybindings: true } }]); + }); }); test('sync profile when the profile is updated to use default profile locally', async () => { - await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); - await client2.sync(); + await runWithFakedTimers({}, async () => { + await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await client2.sync(); - await testObject.sync(await testClient.getResourceManifest()); + await testObject.sync(await testClient.getResourceManifest()); - const profile = testClient.instantiationService.get(IUserDataProfilesService).profiles.find(p => p.id === '1')!; - testClient.instantiationService.get(IUserDataProfilesService).updateProfile(profile, { useDefaultFlags: { keybindings: true } }); + const profile = testClient.instantiationService.get(IUserDataProfilesService).profiles.find(p => p.id === '1')!; + testClient.instantiationService.get(IUserDataProfilesService).updateProfile(profile, { useDefaultFlags: { keybindings: true } }); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', useDefaultFlags: { keybindings: true } }]); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: { keybindings: true } }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', useDefaultFlags: { keybindings: true } }]); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: { keybindings: true } }]); + }); }); test('sync profile when the profile is updated to use default profile remotely', async () => { - const profile = await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); - await client2.sync(); + await runWithFakedTimers({}, async () => { + const profile = await client2.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await client2.sync(); - await testObject.sync(await testClient.getResourceManifest()); + await testObject.sync(await testClient.getResourceManifest()); - client2.instantiationService.get(IUserDataProfilesService).updateProfile(profile, { useDefaultFlags: { keybindings: true } }); - await client2.sync(); + client2.instantiationService.get(IUserDataProfilesService).updateProfile(profile, { useDefaultFlags: { keybindings: true } }); + await client2.sync(); - await testObject.sync(await testClient.getResourceManifest()); - assert.strictEqual(testObject.status, SyncStatus.Idle); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + await testObject.sync(await testClient.getResourceManifest()); + assert.strictEqual(testObject.status, SyncStatus.Idle); + assert.deepStrictEqual(testObject.conflicts.conflicts, []); - const { content } = await testClient.read(testObject.resource); - assert.ok(content !== null); - const actual = parseRemoteProfiles(content); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', useDefaultFlags: { keybindings: true } }]); + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseRemoteProfiles(content); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', useDefaultFlags: { keybindings: true } }]); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: { keybindings: true } }]); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: undefined, useDefaultFlags: { keybindings: true } }]); + }); }); function parseRemoteProfiles(content: string): ISyncUserDataProfile[] { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index b622a3efe44..f83ecab1d9c 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -26,7 +26,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IRequestService } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -95,7 +95,7 @@ export class UserDataSyncClient extends Disposable { const storageService = this._register(new TestStorageService(userDataProfilesService.defaultProfile)); this.instantiationService.stub(IStorageService, this._register(storageService)); - this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(storageService))); + this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(false, storageService))); const configurationService = this._register(new ConfigurationService(userDataProfilesService.defaultProfile.settingsResource, fileService, new NullPolicyService(), logService)); await configurationService.initialize(); @@ -188,6 +188,8 @@ export class UserDataSyncTestServer implements IRequestService { constructor(private readonly rateLimit = Number.MAX_SAFE_INTEGER, private readonly retryAfter?: number) { } async resolveProxy(url: string): Promise { return url; } + async lookupAuthorization(authInfo: AuthInfo): Promise { return undefined; } + async lookupKerberosAuthorization(url: string): Promise { return undefined; } async loadCertificates(): Promise { return []; } async request(options: IRequestOptions, token: CancellationToken): Promise { @@ -355,7 +357,7 @@ export class TestUserDataSyncUtilService implements IUserDataSyncUtilService { _serviceBrand: any; - async resolveDefaultIgnoredSettings(): Promise { + async resolveDefaultCoreIgnoredSettings(): Promise { return getDefaultIgnoredSettings(); } diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 665d09ed41e..a06d711277e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath } from 'vs/base/common/resources'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index db83d62e163..fefa58adf3c 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { newWriteableBufferStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -414,6 +414,8 @@ suite('UserDataSyncRequestsSession', () => { _serviceBrand: undefined, async request() { return { res: { headers: {} }, stream: newWriteableBufferStream() }; }, async resolveProxy() { return undefined; }, + async lookupAuthorization() { return undefined; }, + async lookupKerberosAuthorization() { return undefined; }, async loadCertificates() { return []; } }; diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 8386e94dab2..3b1cee5f90c 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -18,6 +18,7 @@ import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { deepClone } from 'vs/base/common/objects'; import { isWindows } from 'vs/base/common/platform'; import { isUNCAccessRestrictionsDisabled, getUNCHostAllowlist } from 'vs/base/node/unc'; +import { upcast } from 'vs/base/common/types'; export interface IUtilityProcessConfiguration { @@ -76,6 +77,13 @@ export interface IUtilityProcessConfiguration { * the V8 sandbox. */ readonly forceAllocationsToV8Sandbox?: boolean; + + /** + * HTTP 401 and 407 requests created via electron:net module + * will be redirected to the main process and can be handled + * via the app#login event. + */ + readonly respondToAuthRequestsFromMainProcess?: boolean; } export interface IWindowUtilityProcessConfiguration extends IUtilityProcessConfiguration { @@ -168,6 +176,7 @@ export class UtilityProcess extends Disposable { private process: ElectronUtilityProcess | undefined = undefined; private processPid: number | undefined = undefined; private configuration: IUtilityProcessConfiguration | undefined = undefined; + private killed = false; constructor( @ILogService private readonly logService: ILogService, @@ -234,20 +243,25 @@ export class UtilityProcess extends Disposable { const execArgv = this.configuration.execArgv ?? []; const allowLoadingUnsignedLibraries = this.configuration.allowLoadingUnsignedLibraries; const forceAllocationsToV8Sandbox = this.configuration.forceAllocationsToV8Sandbox; + const respondToAuthRequestsFromMainProcess = this.configuration.respondToAuthRequestsFromMainProcess; const stdio = 'pipe'; const env = this.createEnv(configuration); this.log('creating new...', Severity.Info); // Fork utility process - this.process = utilityProcess.fork(modulePath, args, { + this.process = utilityProcess.fork(modulePath, args, upcast({ serviceName, env, execArgv, allowLoadingUnsignedLibraries, forceAllocationsToV8Sandbox, + respondToAuthRequestsFromMainProcess, stdio - } as ForkOptions & { forceAllocationsToV8Sandbox?: Boolean }); + })); // Register to events this.registerListeners(this.process, this.configuration, serviceName); @@ -314,10 +328,11 @@ export class UtilityProcess extends Disposable { // Exit this._register(Event.fromNodeEventEmitter(process, 'exit')(code => { - this.log(`received exit event with code ${code}`, Severity.Info); + const normalizedCode = this.isNormalExit(code) ? 0 : code; + this.log(`received exit event with code ${normalizedCode}`, Severity.Info); // Event - this._onExit.fire({ pid: this.processPid!, code, signal: 'unknown' }); + this._onExit.fire({ pid: this.processPid!, code: normalizedCode, signal: 'unknown' }); // Cleanup this.onDidExitOrCrashOrKill(); @@ -325,14 +340,14 @@ export class UtilityProcess extends Disposable { // Child process gone this._register(Event.fromNodeEventEmitter<{ details: Details }>(app, 'child-process-gone', (event, details) => ({ event, details }))(({ details }) => { - if (details.type === 'Utility' && details.name === serviceName) { + if (details.type === 'Utility' && details.name === serviceName && !this.isNormalExit(details.exitCode)) { this.log(`crashed with code ${details.exitCode} and reason '${details.reason}'`, Severity.Error); // Telemetry type UtilityProcessCrashClassification = { type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The type of utility process to understand the origin of the crash better.' }; reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reason of the utility process crash to understand the nature of the crash better.' }; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The exit code of the utility process to understand the nature of the crash better' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The exit code of the utility process to understand the nature of the crash better' }; owner: 'bpasero'; comment: 'Provides insight into reasons the utility process crashed.'; }; @@ -415,12 +430,24 @@ export class UtilityProcess extends Disposable { const killed = this.process.kill(); if (killed) { this.log('successfully killed the process', Severity.Info); + this.killed = true; this.onDidExitOrCrashOrKill(); } else { this.log('unable to kill the process', Severity.Warning); } } + private isNormalExit(exitCode: number): boolean { + if (exitCode === 0) { + return true; + } + + // Treat an exit code of 15 (SIGTERM) as a normal exit + // if we triggered the termination from process.kill() + + return this.killed && exitCode === 15 /* SIGTERM */; + } + private onDidExitOrCrashOrKill(): void { if (typeof this.processPid === 'number') { UtilityProcess.all.delete(this.processPid); diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts index 70be47e3b27..4888e76a2fe 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts @@ -89,6 +89,7 @@ export class UtilityProcessWorkerMainService extends Disposable implements IUtil this.logService.trace(`[UtilityProcessWorker]: disposeWorker(window: ${configuration.reply.windowId}, moduleId: ${configuration.process.moduleId})`); worker.kill(); + worker.dispose(); this.workers.delete(workerId); } } @@ -98,7 +99,7 @@ class UtilityProcessWorker extends Disposable { private readonly _onDidTerminate = this._register(new Emitter()); readonly onDidTerminate = this._onDidTerminate.event; - private readonly utilityProcess = new WindowUtilityProcess(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService); + private readonly utilityProcess = this._register(new WindowUtilityProcess(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService)); constructor( @ILogService private readonly logService: ILogService, diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 2b991c2b203..2b7ffc4651f 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -49,6 +49,9 @@ export interface IBaseOpenWindowsOptions { * If not set, defaults to the remote authority of the current window. */ readonly remoteAuthority?: string | null; + + readonly forceProfile?: string; + readonly forceTempProfile?: boolean; } export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { @@ -64,9 +67,6 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { readonly gotoLineMode?: boolean; readonly waitMarkerFileURI?: URI; - - readonly forceProfile?: string; - readonly forceTempProfile?: boolean; } export interface IAddFoldersRequest { @@ -158,6 +158,7 @@ export interface IWindowSettings { readonly enableMenuBarMnemonics: boolean; readonly closeWhenEmpty: boolean; readonly clickThroughInactive: boolean; + readonly newWindowProfile: string; readonly density: IDensitySettings; } @@ -221,6 +222,8 @@ export function getTitleBarStyle(configurationService: IConfigurationService): T return isLinux ? TitlebarStyle.NATIVE : TitlebarStyle.CUSTOM; // default to custom on all macOS and Windows } +export const DEFAULT_CUSTOM_TITLEBAR_HEIGHT = 35; // includes space for command center + export function useWindowControlsOverlay(configurationService: IConfigurationService): boolean { if (!isWindows || isWeb) { return false; // only supported on a desktop Windows instance @@ -346,6 +349,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native machineId: string; sqmId: string; + devDeviceId: string; execPath: string; backupPath?: string; diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index 04795036c44..15fcb1790fe 100644 --- a/src/vs/platform/window/electron-main/window.ts +++ b/src/vs/platform/window/electron-main/window.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, Rectangle, screen } from 'electron'; +import electron from 'electron'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -23,7 +23,7 @@ export interface IBaseWindow extends IDisposable { readonly onDidClose: Event; readonly id: number; - readonly win: BrowserWindow | null; + readonly win: electron.BrowserWindow | null; readonly lastFocusTime: number; focus(options?: { force: boolean }): void; @@ -40,6 +40,8 @@ export interface IBaseWindow extends IDisposable { toggleFullScreen(): void; updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): void; + + matches(webContents: electron.WebContents): boolean; } export interface ICodeWindow extends IBaseWindow { @@ -74,7 +76,7 @@ export interface ICodeWindow extends IBaseWindow { close(): void; - getBounds(): Rectangle; + getBounds(): electron.Rectangle; send(channel: string, ...args: any[]): void; sendWhenReady(channel: string, token: CancellationToken, ...args: any[]): void; @@ -156,7 +158,7 @@ export const defaultAuxWindowState = function (): IWindowState { const width = 800; const height = 600; - const workArea = screen.getPrimaryDisplay().workArea; + const workArea = electron.screen.getPrimaryDisplay().workArea; const x = Math.max(workArea.x + (workArea.width / 2) - (width / 2), 0); const y = Math.max(workArea.y + (workArea.height / 2) - (height / 2), 0); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index e46474de06c..a4330686cdb 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, Display, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl } from 'electron'; +import electron, { BrowserWindowConstructorOptions } from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -32,7 +32,7 @@ import { IApplicationStorageMainService, IStorageMainService } from 'vs/platform import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ThemeIcon } from 'vs/base/common/themables'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { getMenuBarVisibility, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay } from 'vs/platform/window/common/window'; +import { getMenuBarVisibility, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, TitlebarStyle } from 'vs/platform/window/common/window'; import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext, WindowStateValidator } from 'vs/platform/windows/electron-main/windows'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; @@ -51,7 +51,7 @@ export interface IWindowCreationOptions { readonly isExtensionTestHost?: boolean; } -interface ITouchBarSegment extends SegmentedControlSegment { +interface ITouchBarSegment extends electron.SegmentedControlSegment { readonly id: string; } @@ -111,9 +111,9 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { protected _lastFocusTime = Date.now(); // window is shown on creation so take current time get lastFocusTime(): number { return this._lastFocusTime; } - protected _win: BrowserWindow | null = null; + protected _win: electron.BrowserWindow | null = null; get win() { return this._win; } - protected setWin(win: BrowserWindow): void { + protected setWin(win: electron.BrowserWindow, options?: BrowserWindowConstructorOptions): void { this._win = win; // Window Events @@ -131,16 +131,18 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { this._register(Event.fromNodeEventEmitter(this._win, 'leave-full-screen')(() => this._onDidLeaveFullScreen.fire())); // Sheet Offsets - const useCustomTitleStyle = !hasNativeTitlebar(this.configurationService); + const useCustomTitleStyle = !hasNativeTitlebar(this.configurationService, options?.titleBarStyle === 'hidden' ? TitlebarStyle.CUSTOM : undefined /* unknown */); if (isMacintosh && useCustomTitleStyle) { win.setSheetOffset(isBigSurOrNewer(release()) ? 28 : 22); // offset dialogs by the height of the custom title bar if we have any } - // Update the window controls immediately based on cached values + // Update the window controls immediately based on cached or default values if (useCustomTitleStyle && ((isWindows && useWindowControlsOverlay(this.configurationService)) || isMacintosh)) { const cachedWindowControlHeight = this.stateService.getItem((BaseWindow.windowControlHeightStateStorageKey)); if (cachedWindowControlHeight) { this.updateWindowControls({ height: cachedWindowControlHeight }); + } else { + this.updateWindowControls({ height: DEFAULT_CUSTOM_TITLEBAR_HEIGHT }); } } @@ -158,7 +160,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { // This sets up a listener for the window hook. This is a Windows-only API provided by electron. win.hookWindowMessage(WM_INITMENU, () => { const [x, y] = win.getPosition(); - const cursorPos = screen.getCursorScreenPoint(); + const cursorPos = electron.screen.getCursorScreenPoint(); const cx = cursorPos.x - x; const cy = cursorPos.y - y; @@ -216,6 +218,49 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { super(); } + protected applyState(state: IWindowState, hasMultipleDisplays = electron.screen.getAllDisplays().length > 0): void { + + // TODO@electron (Electron 4 regression): when running on multiple displays where the target display + // to open the window has a larger resolution than the primary display, the window will not size + // correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872) + // + // Extended to cover Windows as well as Mac (https://github.com/microsoft/vscode/issues/146499) + // + // However, when running with native tabs with multiple windows we cannot use this workaround + // because there is a potential that the new window will be added as native tab instead of being + // a window on its own. In that case calling setBounds() would cause https://github.com/microsoft/vscode/issues/75830 + + const windowSettings = this.configurationService.getValue('window'); + const useNativeTabs = isMacintosh && windowSettings?.nativeTabs === true; + if ((isMacintosh || isWindows) && hasMultipleDisplays && (!useNativeTabs || electron.BrowserWindow.getAllWindows().length === 1)) { + if ([state.width, state.height, state.x, state.y].every(value => typeof value === 'number')) { + this._win?.setBounds({ + width: state.width, + height: state.height, + x: state.x, + y: state.y + }); + } + } + + if (state.mode === WindowMode.Maximized || state.mode === WindowMode.Fullscreen) { + + // this call may or may not show the window, depends + // on the platform: currently on Windows and Linux will + // show the window as active. To be on the safe side, + // we show the window at the end of this block. + this._win?.maximize(); + + if (state.mode === WindowMode.Fullscreen) { + this.setFullScreen(true, true); + } + + // to reduce flicker from the default window size + // to maximize or fullscreen, we only show after + this._win?.show(); + } + } + private representedFilename: string | undefined; setRepresentedFilename(filename: string): void { @@ -254,7 +299,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { focus(options?: { force: boolean }): void { if (isMacintosh && options?.force) { - app.focus({ steal: true }); + electron.app.focus({ steal: true }); } const win = this.win; @@ -277,7 +322,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { // Respect system settings on mac with regards to title click on windows title if (isMacintosh) { - const action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); + const action = electron.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); switch (action) { case 'Minimize': win.minimize(); @@ -304,7 +349,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { } } - //#region WCO + //#region Window Control Overlays private static readonly windowControlHeightStateStorageKey = 'windowControlHeight'; @@ -449,6 +494,8 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { //#endregion + abstract matches(webContents: electron.WebContents): boolean; + override dispose(): void { super.dispose(); @@ -477,7 +524,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private _id: number; get id(): number { return this._id; } - protected override _win: BrowserWindow; + protected override _win: electron.BrowserWindow; get backupPath(): string | undefined { return this._config?.backupPath; } @@ -514,7 +561,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = []; - private readonly touchBarGroups: TouchBarSegmentedControl[] = []; + private readonly touchBarGroups: electron.TouchBarSegmentedControl[] = []; private currentHttpProxy: string | undefined = undefined; private currentNoProxy: string | undefined = undefined; @@ -557,67 +604,22 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.windowState = state; this.logService.trace('window#ctor: using window state', state); - // In case we are maximized or fullscreen, only show later - // after the call to maximize/fullscreen (see below) - const isFullscreenOrMaximized = (this.windowState.mode === WindowMode.Maximized || this.windowState.mode === WindowMode.Fullscreen); - - const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, { - show: !isFullscreenOrMaximized, // reduce flicker by showing later - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath, - additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], - v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', - } + const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, undefined, { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath, + additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], + v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', }); // Create the browser window mark('code/willCreateCodeBrowserWindow'); - this._win = new BrowserWindow(options); + this._win = new electron.BrowserWindow(options); mark('code/didCreateCodeBrowserWindow'); this._id = this._win.id; - this.setWin(this._win); + this.setWin(this._win, options); - // TODO@electron (Electron 4 regression): when running on multiple displays where the target display - // to open the window has a larger resolution than the primary display, the window will not size - // correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872) - // - // Extended to cover Windows as well as Mac (https://github.com/microsoft/vscode/issues/146499) - // - // However, when running with native tabs with multiple windows we cannot use this workaround - // because there is a potential that the new window will be added as native tab instead of being - // a window on its own. In that case calling setBounds() would cause https://github.com/microsoft/vscode/issues/75830 - const windowSettings = this.configurationService.getValue('window'); - const useNativeTabs = isMacintosh && windowSettings?.nativeTabs === true; - if ((isMacintosh || isWindows) && hasMultipleDisplays && (!useNativeTabs || BrowserWindow.getAllWindows().length === 1)) { - if ([this.windowState.width, this.windowState.height, this.windowState.x, this.windowState.y].every(value => typeof value === 'number')) { - this._win.setBounds({ - width: this.windowState.width, - height: this.windowState.height, - x: this.windowState.x, - y: this.windowState.y - }); - } - } - - if (isFullscreenOrMaximized) { - mark('code/willMaximizeCodeWindow'); - - // this call may or may not show the window, depends - // on the platform: currently on Windows and Linux will - // show the window as active. To be on the safe side, - // we show the window at the end of this block. - this._win.maximize(); - - if (this.windowState.mode === WindowMode.Fullscreen) { - this.setFullScreen(true, true); - } - - // to reduce flicker from the default window size - // to maximize or fullscreen, we only show after - this._win.show(); - mark('code/didMaximizeCodeWindow'); - } + // Apply some state after window creation + this.applyState(this.windowState, hasMultipleDisplays); this._lastFocusTime = Date.now(); // since we show directly, we need to set the last focus time too } @@ -682,21 +684,19 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private registerListeners(): void { // Window error conditions to handle - this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE)); - this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.PROCESS_GONE, { ...details })); - this._win.webContents.on('did-fail-load', (event, exitCode, reason) => this.onWindowError(WindowError.LOAD, { reason, exitCode })); + this._register(Event.fromNodeEventEmitter(this._win, 'unresponsive')(() => this.onWindowError(WindowError.UNRESPONSIVE))); + this._register(Event.fromNodeEventEmitter(this._win.webContents, 'render-process-gone', (event, details) => details)(details => this.onWindowError(WindowError.PROCESS_GONE, { ...details }))); + this._register(Event.fromNodeEventEmitter(this._win.webContents, 'did-fail-load', (event, exitCode, reason) => ({ exitCode, reason }))(({ exitCode, reason }) => this.onWindowError(WindowError.LOAD, { reason, exitCode }))); // Prevent windows/iframes from blocking the unload // through DOM events. We have our own logic for // unloading a window that should not be confused // with the DOM way. // (https://github.com/microsoft/vscode/issues/122736) - this._win.webContents.on('will-prevent-unload', event => { - event.preventDefault(); - }); + this._register(Event.fromNodeEventEmitter(this._win.webContents, 'will-prevent-unload')(event => event.preventDefault())); // Remember that we loaded - this._win.webContents.on('did-finish-load', () => { + this._register(Event.fromNodeEventEmitter(this._win.webContents, 'did-finish-load')(() => { // Associate properties from the load request if provided if (this.pendingLoadConfig) { @@ -704,7 +704,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.pendingLoadConfig = undefined; } - }); + })); // Window (Un)Maximize this._register(this.onDidMaximize(() => { @@ -778,9 +778,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Telemetry type WindowErrorClassification = { - type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The type of window error to understand the nature of the error better.' }; + type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The type of window error to understand the nature of the error better.' }; reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reason of the window error to understand the nature of the error better.' }; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The exit code of the window process to understand the nature of the error better' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The exit code of the window process to understand the nature of the error better' }; owner: 'bpasero'; comment: 'Provides insight into reasons the vscode window had an error.'; }; @@ -957,16 +957,25 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } // Proxy - if (!e || e.affectsConfiguration('http.proxy')) { + if (!e || e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.noProxy')) { let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; + if (newHttpProxy?.indexOf('@') !== -1) { + const uri = URI.parse(newHttpProxy!); + const i = uri.authority.indexOf('@'); + if (i !== -1) { + newHttpProxy = uri.with({ authority: uri.authority.substring(i + 1) }) + .toString(); + } + } if (newHttpProxy?.endsWith('/')) { newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); } - const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. + const newNoProxy = (this.configurationService.getValue('http.noProxy') || []).map((item) => item.trim()).join(',') + || (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) { this.currentHttpProxy = newHttpProxy; this.currentNoProxy = newNoProxy; @@ -975,13 +984,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); - type appWithProxySupport = Electron.App & { - setProxy(config: Electron.Config): Promise; - resolveProxy(url: string): Promise; - }; - if (typeof (app as appWithProxySupport).setProxy === 'function') { - (app as appWithProxySupport).setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); - } + electron.app.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } } @@ -1132,7 +1135,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { configuration['extensions-dir'] = cli['extensions-dir']; } - configuration.accessibilitySupport = app.isAccessibilitySupportEnabled(); + configuration.accessibilitySupport = electron.app.isAccessibilitySupportEnabled(); configuration.isInitialStartup = false; // since this is a reload configuration.policiesData = this.policyService.serialize(); // set policies data again configuration.continueOn = this.environmentMainService.continueOn; @@ -1186,9 +1189,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // fullscreen gets special treatment if (this.isFullScreen) { - let display: Display | undefined; + let display: electron.Display | undefined; try { - display = screen.getDisplayMatching(this.getBounds()); + display = electron.screen.getDisplayMatching(this.getBounds()); } catch (error) { // Electron has weird conditions under which it throws errors // e.g. https://github.com/microsoft/vscode/issues/100334 when @@ -1233,7 +1236,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // only consider non-minimized window states if (mode === WindowMode.Normal || mode === WindowMode.Maximized) { - let bounds: Rectangle; + let bounds: electron.Rectangle; if (mode === WindowMode.Normal) { bounds = this.getBounds(); } else { @@ -1262,7 +1265,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Window dimensions try { - const displays = screen.getAllDisplays(); + const displays = electron.screen.getAllDisplays(); hasMultipleDisplays = displays.length > 1; state = WindowStateValidator.validateWindowState(this.logService, state, displays); @@ -1276,7 +1279,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { return [state || defaultWindowState(), hasMultipleDisplays]; } - getBounds(): Rectangle { + getBounds(): electron.Rectangle { const [x, y] = this._win.getPosition(); const [width, height] = this._win.getSize(); @@ -1425,16 +1428,16 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.touchBarGroups.push(groupTouchBar); } - this._win.setTouchBar(new TouchBar({ items: this.touchBarGroups })); + this._win.setTouchBar(new electron.TouchBar({ items: this.touchBarGroups })); } - private createTouchBarGroup(items: ISerializableCommandAction[] = []): TouchBarSegmentedControl { + private createTouchBarGroup(items: ISerializableCommandAction[] = []): electron.TouchBarSegmentedControl { // Group Segments const segments = this.createTouchBarGroupSegments(items); // Group Control - const control = new TouchBar.TouchBarSegmentedControl({ + const control = new electron.TouchBar.TouchBarSegmentedControl({ segments, mode: 'buttons', segmentStyle: 'automatic', @@ -1448,9 +1451,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private createTouchBarGroupSegments(items: ISerializableCommandAction[] = []): ITouchBarSegment[] { const segments: ITouchBarSegment[] = items.map(item => { - let icon: NativeImage | undefined; + let icon: electron.NativeImage | undefined; if (item.icon && !ThemeIcon.isThemeIcon(item.icon) && item.icon?.dark?.scheme === Schemas.file) { - icon = nativeImage.createFromPath(URI.revive(item.icon.dark).fsPath); + icon = electron.nativeImage.createFromPath(URI.revive(item.icon.dark).fsPath); if (icon.isEmpty()) { icon = undefined; } @@ -1473,6 +1476,10 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { return segments; } + matches(webContents: electron.WebContents): boolean { + return this._win?.webContents.id === webContents.id; + } + override dispose(): void { super.dispose(); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 0cccca7c810..774ec18be10 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -3,22 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindowConstructorOptions, Display, Rectangle, WebContents, screen } from 'electron'; +import electron from 'electron'; +import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; +import { join } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICodeWindow, IWindowState, WindowMode, defaultWindowState } from 'vs/platform/window/electron-main/window'; -import { IOpenEmptyWindowOptions, IWindowOpenable, IWindowSettings, WindowMinimumSize, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; -import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { join } from 'vs/base/common/path'; import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow'; -import { Color } from 'vs/base/common/color'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; +import { IOpenEmptyWindowOptions, IWindowOpenable, IWindowSettings, TitlebarStyle, WindowMinimumSize, hasNativeTitlebar, useNativeFullScreen, useWindowControlsOverlay, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { ICodeWindow, IWindowState, WindowMode, defaultWindowState } from 'vs/platform/window/electron-main/window'; export const IWindowsMainService = createDecorator('windowsMainService'); @@ -53,7 +53,7 @@ export interface IWindowsMainService { getLastActiveWindow(): ICodeWindow | undefined; getWindowById(windowId: number): ICodeWindow | undefined; - getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined; + getWindowByWebContents(webContents: electron.WebContents): ICodeWindow | undefined; } export interface IWindowsCountChangedEvent { @@ -115,7 +115,12 @@ export interface IOpenConfiguration extends IBaseOpenConfiguration { export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { } -export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState: IWindowState, overrides?: BrowserWindowConstructorOptions): BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { +export interface IDefaultBrowserWindowOptionsOverrides { + forceNativeTitlebar?: boolean; + disableFullscreen?: boolean; +} + +export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState: IWindowState, overrides?: IDefaultBrowserWindowOptionsOverrides, webPreferences?: electron.WebPreferences): electron.BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { const themeMainService = accessor.get(IThemeMainService); const productService = accessor.get(IProductService); const configurationService = accessor.get(IConfigurationService); @@ -123,17 +128,18 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt const windowSettings = configurationService.getValue('window'); - const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { + const options: electron.BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { backgroundColor: themeMainService.getBackgroundColor(), minWidth: WindowMinimumSize.WIDTH, minHeight: WindowMinimumSize.HEIGHT, title: productService.nameLong, - ...overrides, + show: windowState.mode !== WindowMode.Maximized && windowState.mode !== WindowMode.Fullscreen, // reduce flicker by showing later x: windowState.x, y: windowState.y, width: windowState.width, height: windowState.height, webPreferences: { + ...webPreferences, enableWebSQL: false, spellcheck: false, zoomFactor: zoomLevelToZoomFactor(windowState.zoomLevel ?? windowSettings?.zoomLevel), @@ -141,7 +147,6 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt // Enable experimental css highlight api https://chromestatus.com/feature/5436441440026624 // Refs https://github.com/microsoft/vscode/issues/140098 enableBlinkFeatures: 'HighlightAPI', - ...overrides?.webPreferences, sandbox: true }, experimentalDarkMode: true @@ -161,7 +166,9 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt } } - if (isMacintosh && !useNativeFullScreen(configurationService)) { + if (overrides?.disableFullscreen) { + options.fullscreen = false; + } else if (isMacintosh && !useNativeFullScreen(configurationService)) { options.fullscreenable = false; // enables simple fullscreen mode } @@ -170,7 +177,7 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt options.tabbingIdentifier = productService.nameShort; // this opts in to sierra tabs } - const hideNativeTitleBar = !hasNativeTitlebar(configurationService); + const hideNativeTitleBar = !hasNativeTitlebar(configurationService, overrides?.forceNativeTitlebar ? TitlebarStyle.NATIVE : undefined); if (hideNativeTitleBar) { options.titleBarStyle = 'hidden'; if (!isMacintosh) { @@ -215,7 +222,7 @@ export function getLastFocused(windows: ICodeWindow[] | IAuxiliaryWindow[]): ICo export namespace WindowStateValidator { - export function validateWindowState(logService: ILogService, state: IWindowState, displays = screen.getAllDisplays()): IWindowState | undefined { + export function validateWindowState(logService: ILogService, state: IWindowState, displays = electron.screen.getAllDisplays()): IWindowState | undefined { logService.trace(`window#validateWindowState: validating window state on ${displays.length} display(s)`, state); if ( @@ -313,10 +320,10 @@ export namespace WindowStateValidator { } // Multi Monitor (non-fullscreen): ensure window is within display bounds - let display: Display | undefined; - let displayWorkingArea: Rectangle | undefined; + let display: electron.Display | undefined; + let displayWorkingArea: electron.Rectangle | undefined; try { - display = screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height }); + display = electron.screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height }); displayWorkingArea = getWorkingArea(display); logService.trace('window#validateWindowState: multi-monitor working area', displayWorkingArea); @@ -343,7 +350,7 @@ export namespace WindowStateValidator { return undefined; } - function getWorkingArea(display: Display): Rectangle | undefined { + function getWorkingArea(display: electron.Display): electron.Rectangle | undefined { // Prefer the working area of the display to account for taskbars on the // desktop being positioned somewhere (https://github.com/microsoft/vscode/issues/50830). diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 5ebe9212d44..5e6279486fc 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { app, BrowserWindow, WebContents, shell } from 'electron'; -import { Promises } from 'vs/base/node/pfs'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; import { hostname, release, arch } from 'os'; import { coalesce, distinct } from 'vs/base/common/arrays'; @@ -211,6 +211,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic constructor( private readonly machineId: string, private readonly sqmId: string, + private readonly devDeviceId: string, private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, @ILoggerMainService private readonly loggerService: ILoggerMainService, @@ -268,7 +269,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority }); + return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority, forceTempProfile: options?.forceTempProfile, forceProfile: options?.forceProfile }); } openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void { @@ -667,7 +668,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow && focusedWindow.id !== mainWindow.id) { - const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowById(focusedWindow.id); + const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowByWebContents(focusedWindow.webContents); if (auxiliaryWindowCandidate && auxiliaryWindowCandidate.parentId === mainWindow.id) { windowToFocus = auxiliaryWindowCandidate; } @@ -1056,7 +1057,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic path = sanitizeFilePath(normalize(path), cwd()); try { - const pathStat = await Promises.stat(path); + const pathStat = await fs.promises.stat(path); // File if (pathStat.isFile()) { @@ -1389,7 +1390,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const windowConfig = this.configurationService.getValue('window'); const lastActiveWindow = this.getLastActiveWindow(); - const defaultProfile = lastActiveWindow?.profile ?? this.userDataProfilesMainService.defaultProfile; + const newWindowProfile = windowConfig?.newWindowProfile + ? this.userDataProfilesMainService.profiles.find(profile => profile.name === windowConfig.newWindowProfile) : undefined; + const defaultProfile = newWindowProfile ?? lastActiveWindow?.profile ?? this.userDataProfilesMainService.defaultProfile; let window: ICodeWindow | undefined; if (!options.forceNewWindow && !options.forceNewTabbedWindow) { @@ -1409,6 +1412,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic machineId: this.machineId, sqmId: this.sqmId, + devDeviceId: this.devDeviceId, windowId: -1, // Will be filled in by the window once loaded later @@ -1440,6 +1444,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic workspace: options.workspace, userEnv: { ...this.initialUserEnv, ...options.userEnv }, + nls: { + messages: globalThis._VSCODE_NLS_MESSAGES, + language: globalThis._VSCODE_NLS_LANGUAGE + }, + filesToOpenOrCreate: options.filesToOpen?.filesToOpenOrCreate, filesToDiff: options.filesToOpen?.filesToDiff, filesToMerge: options.filesToOpen?.filesToMerge, @@ -1506,7 +1515,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const webContents = assertIsDefined(createdWindow.win?.webContents); webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own - webContents.on('devtools-reload-page', () => this.lifecycleMainService.reload(createdWindow)); + disposables.add(Event.fromNodeEventEmitter(webContents, 'devtools-reload-page')(() => this.lifecycleMainService.reload(createdWindow))); // Lifecycle this.lifecycleMainService.registerWindow(createdWindow); @@ -1701,6 +1710,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return undefined; } - return this.getWindowById(browserWindow.id); + const window = this.getWindowById(browserWindow.id); + + return window?.matches(webContents) ? window : undefined; } } diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts index df866ea3556..6a6591cd44d 100644 --- a/src/vs/platform/windows/electron-main/windowsStateHandler.ts +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, Display, screen } from 'electron'; +import electron from 'electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; @@ -78,26 +78,26 @@ export class WindowsStateHandler extends Disposable { // When a window looses focus, save all windows state. This allows to // prevent loss of window-state data when OS is restarted without properly // shutting down the application (https://github.com/microsoft/vscode/issues/87171) - app.on('browser-window-blur', () => { + electron.app.on('browser-window-blur', () => { if (!this.shuttingDown) { this.saveWindowsState(); } }); // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeCloseWindow(window => this.onBeforeCloseWindow(window)); - this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.windowsMainService.onDidChangeWindowsCount(e => { + this._register(this.lifecycleMainService.onBeforeCloseWindow(window => this.onBeforeCloseWindow(window))); + this._register(this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown())); + this._register(this.windowsMainService.onDidChangeWindowsCount(e => { if (e.newCount - e.oldCount > 0) { // clear last closed window state when a new window opens. this helps on macOS where // otherwise closing the last window, opening a new window and then quitting would // use the state of the previously closed window when restarting. this.lastClosedState = undefined; } - }); + })); // try to save state before destroy because close will not fire - this.windowsMainService.onDidDestroyWindow(window => this.onBeforeCloseWindow(window)); + this._register(this.windowsMainService.onDidDestroyWindow(window => this.onBeforeCloseWindow(window))); } // Note that onBeforeShutdown() and onBeforeCloseWindow() are fired in different order depending on the OS: @@ -339,8 +339,8 @@ export class WindowsStateHandler extends Disposable { // // We want the new window to open on the same display that the last active one is in - let displayToUse: Display | undefined; - const displays = screen.getAllDisplays(); + let displayToUse: electron.Display | undefined; + const displays = electron.screen.getAllDisplays(); // Single Display if (displays.length === 1) { @@ -352,18 +352,18 @@ export class WindowsStateHandler extends Disposable { // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is if (isMacintosh) { - const cursorPoint = screen.getCursorScreenPoint(); - displayToUse = screen.getDisplayNearestPoint(cursorPoint); + const cursorPoint = electron.screen.getCursorScreenPoint(); + displayToUse = electron.screen.getDisplayNearestPoint(cursorPoint); } // if we have a last active window, use that display for the new window if (!displayToUse && lastActive) { - displayToUse = screen.getDisplayMatching(lastActive.getBounds()); + displayToUse = electron.screen.getDisplayMatching(lastActive.getBounds()); } // fallback to primary display or first display if (!displayToUse) { - displayToUse = screen.getPrimaryDisplay() || displays[0]; + displayToUse = electron.screen.getPrimaryDisplay() || displays[0]; } } diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index dbba00ae8f0..f3ce84f9c7a 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { join } from 'vs/base/common/path'; @@ -75,6 +75,7 @@ suite('WindowsFinder', () => { serializeWindowState(): IWindowState { throw new Error('Method not implemented'); } updateWindowControls(options: { height?: number | undefined; backgroundColor?: string | undefined; foregroundColor?: string | undefined }): void { throw new Error('Method not implemented.'); } notifyZoomLevel(level: number): void { throw new Error('Method not implemented.'); } + matches(webContents: any): boolean { throw new Error('Method not implemented.'); } dispose(): void { } }; } diff --git a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index 0b96b1bf740..65d1a9937f9 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 805e36a654f..7988f9a930f 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -6,7 +6,6 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export enum WorkspaceTrustScope { @@ -14,14 +13,6 @@ export enum WorkspaceTrustScope { Remote = 1 } -export function workspaceTrustToString(trustState: boolean) { - if (trustState) { - return localize('trusted', "Trusted"); - } else { - return localize('untrusted', "Restricted Mode"); - } -} - export interface WorkspaceTrustRequestButton { readonly label: string; readonly type: 'ContinueWithTrust' | 'ContinueWithoutTrust' | 'Manage' | 'Cancel'; diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 2464d5e4a35..fb8c1cf18e6 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { join } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 25a017e19a9..a6f7fc430bc 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -300,7 +300,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Exclude some very common files from the dock/taskbar private static readonly COMMON_FILES_FILTER = [ 'COMMIT_EDITMSG', - 'MERGE_MSG' + 'MERGE_MSG', + 'git-rebase-todo' ]; private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 236f6d6fc32..bfb4fcd5145 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow } from 'electron'; +import * as fs from 'fs'; +import electron from 'electron'; import { Emitter, Event } from 'vs/base/common/event'; import { parse } from 'vs/base/common/json'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -102,7 +103,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork } resolveLocalWorkspace(uri: URI): Promise { - return this.doResolveLocalWorkspace(uri, path => Promises.readFile(path, 'utf8')); + return this.doResolveLocalWorkspace(uri, path => fs.promises.readFile(path, 'utf8')); } private doResolveLocalWorkspace(uri: URI, contentsFn: (path: string) => string): IResolvedWorkspace | undefined; @@ -169,7 +170,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); const configPath = workspace.configPath.fsPath; - await Promises.mkdir(dirname(configPath), { recursive: true }); + await fs.promises.mkdir(dirname(configPath), { recursive: true }); await Promises.writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); this.untitledWorkspaces.push({ workspace, remoteAuthority }); @@ -280,7 +281,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork buttons: [localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)), detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.") - }, BrowserWindow.getFocusedWindow() ?? undefined); + }, electron.BrowserWindow.getFocusedWindow() ?? undefined); return false; } diff --git a/src/vs/platform/workspaces/test/common/workspaces.test.ts b/src/vs/platform/workspaces/test/common/workspaces.test.ts index a1f2f262d00..a08f1035ce1 100644 --- a/src/vs/platform/workspaces/test/common/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/common/workspaces.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, reviveIdentifier, hasWorkspaceFileExtension, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IEmptyWorkspaceIdentifier, toWorkspaceIdentifier, isEmptyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; diff --git a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts index 56e4d92fdc9..553be4fa37c 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'vs/base/common/path'; @@ -23,7 +23,7 @@ flakySuite('Workspaces', () => { setup(async () => { testDir = getRandomTestPath(tmpDir, 'vsctests', 'workspacesmanagementmainservice'); - return pfs.Promises.mkdir(testDir, { recursive: true }); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index 2677ffba87d..f2102c3b098 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 0234ce57bb9..92f1ac9f589 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import { isUNC, toSlashes } from 'vs/base/common/extpath'; @@ -112,7 +112,7 @@ flakySuite('WorkspacesManagementMainService', () => { const fileService = new FileService(logService); service = new WorkspacesManagementMainService(environmentMainService, logService, new UserDataProfilesMainService(new StateService(SaveStrategy.DELAYED, environmentMainService, logService, fileService), new UriIdentityService(fileService), environmentMainService, fileService, logService), new TestBackupMainService(), new TestDialogMainService()); - return pfs.Promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); + return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); teardown(() => { diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index f259ea2cbaf..f345bd69a2d 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -5,23 +5,23 @@ import * as cp from 'child_process'; import * as net from 'net'; -import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; -import { FileAccess } from 'vs/base/common/network'; -import { join, delimiter } from 'vs/base/common/path'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; -import { createRandomIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { delimiter, join } from 'vs/base/common/path'; import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { removeDangerousEnvVariables } from 'vs/base/common/processes'; -import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IPCExtHostConnection, writeExtHostConnection, SocketExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { createRandomIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; +import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; +import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; +import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; +import { IPCExtHostConnection, SocketExtHostConnection, writeExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, withUserShellEnvironment: boolean, language: string, environmentService: IServerEnvironmentService, logService: ILogService, configurationService: IConfigurationService): Promise { const nlsConfig = await getNLSConfiguration(language, environmentService.userDataPath); @@ -43,7 +43,7 @@ export async function buildUserEnvironment(startParamsEnv: { [key: string]: stri ...{ VSCODE_AMD_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess', VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true', - VSCODE_NLS_CONFIG: JSON.stringify(nlsConfig, undefined, 0) + VSCODE_NLS_CONFIG: JSON.stringify(nlsConfig) }, ...startParamsEnv }; @@ -103,7 +103,7 @@ class ConnectionData { } } -export class ExtensionHostConnection { +export class ExtensionHostConnection extends Disposable { private _onClose = new Emitter(); readonly onClose: Event = this._onClose.event; @@ -124,6 +124,7 @@ export class ExtensionHostConnection { @IExtensionHostStatusService private readonly _extensionHostStatusService: IExtensionHostStatusService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { + super(); this._canSendSocket = (!isWindows || !this._environmentService.args['socket-path']); this._disposed = false; this._remoteAddress = remoteAddress; @@ -133,6 +134,11 @@ export class ExtensionHostConnection { this._log(`New connection established.`); } + override dispose(): void { + this._cleanResources(); + super.dispose(); + } + private get _logPrefix(): string { return `[${this._remoteAddress}][${this._reconnectionToken.substr(0, 8)}][ExtensionHostConnection] `; } @@ -271,8 +277,8 @@ export class ExtensionHostConnection { this._extensionHostProcess.stderr!.setEncoding('utf8'); const onStdout = Event.fromNodeEventEmitter(this._extensionHostProcess.stdout!, 'data'); const onStderr = Event.fromNodeEventEmitter(this._extensionHostProcess.stderr!, 'data'); - onStdout((e) => this._log(`<${pid}> ${e}`)); - onStderr((e) => this._log(`<${pid}> ${e}`)); + this._register(onStdout((e) => this._log(`<${pid}> ${e}`))); + this._register(onStderr((e) => this._log(`<${pid}> ${e}`))); // Lifecycle this._extensionHostProcess.on('error', (err) => { diff --git a/src/vs/server/node/extensionsScannerService.ts b/src/vs/server/node/extensionsScannerService.ts index 5430ae19162..78dc3bce4f0 100644 --- a/src/vs/server/node/extensionsScannerService.ts +++ b/src/vs/server/node/extensionsScannerService.ts @@ -14,7 +14,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; +import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; export class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { @@ -38,9 +38,9 @@ export class ExtensionsScannerService extends AbstractExtensionsScannerService i protected async getTranslations(language: string): Promise { const config = await getNLSConfiguration(language, this.nativeEnvironmentService.userDataPath); - if (InternalNLSConfiguration.is(config)) { + if (config.languagePack) { try { - const content = await this.fileService.readFile(URI.file(config._translationsConfigFile)); + const content = await this.fileService.readFile(URI.file(config.languagePack.translationsConfigFile)); return JSON.parse(content.value.toString()); } catch (err) { /* Ignore error */ } } diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 84664bbb39a..d9bb0122f64 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -67,7 +67,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { private readonly _serverRootPath: string; - private shutdownTimer: NodeJS.Timer | undefined; + private shutdownTimer: NodeJS.Timeout | undefined; constructor( private readonly _socketServer: SocketServer, @@ -505,6 +505,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { this._extHostConnections[reconnectionToken] = con; this._allReconnectionTokens.add(reconnectionToken); con.onClose(() => { + con.dispose(); delete this._extHostConnections[reconnectionToken]; this._onDidCloseExtHostConnection(); }); @@ -695,7 +696,7 @@ export async function createServer(address: string | net.AddressInfo | null, arg let didLogAboutSIGPIPE = false; process.on('SIGPIPE', () => { // See https://github.com/microsoft/vscode-remote-release/issues/6543 - // We would normally install a SIGPIPE listener in bootstrap.js + // We would normally install a SIGPIPE listener in bootstrap-node.js // But in certain situations, the console itself can be in a broken pipe state // so logging SIGPIPE to the console will cause an infinite async loop if (!didLogAboutSIGPIPE) { diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index 95e9c62afc8..7b58140ea1b 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -79,7 +79,13 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return this._whenExtensionsReady; } - async scanExtensions(language?: string, profileLocation?: URI, extensionDevelopmentLocations?: URI[], languagePackId?: string): Promise { + async scanExtensions( + language?: string, + profileLocation?: URI, + workspaceExtensionLocations?: URI[], + extensionDevelopmentLocations?: URI[], + languagePackId?: string + ): Promise { performance.mark('code/server/willScanExtensions'); this._logService.trace(`Scanning extensions using UI language: ${language}`); @@ -88,7 +94,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS const extensionDevelopmentPaths = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; profileLocation = profileLocation ?? this._userDataProfilesService.defaultProfile.extensionsResource; - const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths, languagePackId); + const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, workspaceExtensionLocations, extensionDevelopmentPaths, languagePackId); this._logService.trace('Scanned Extensions', extensions); this._massageWhenConditions(extensions); @@ -97,36 +103,17 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return extensions; } - async scanSingleExtension(extensionLocation: URI, isBuiltin: boolean, language?: string): Promise { - await this._whenBuiltinExtensionsReady; - - const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null; - - if (!extensionPath) { - return null; - } - - const extension = await this._scanSingleExtension(extensionPath, isBuiltin, language ?? platform.language); - - if (!extension) { - return null; - } - - this._massageWhenConditions([extension]); - - return extension; - } - - private async _scanExtensions(profileLocation: URI, language: string, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { + private async _scanExtensions(profileLocation: URI, language: string, workspaceInstalledExtensionLocations: URI[] | undefined, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { await this._ensureLanguagePackIsInstalled(language, languagePackId); - const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ + const [builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions] = await Promise.all([ this._scanBuiltinExtensions(language), this._scanInstalledExtensions(profileLocation, language), + this._scanWorkspaceInstalledExtensions(language, workspaceInstalledExtensionLocations), this._scanDevelopedExtensions(language, extensionDevelopmentPath) ]); - return dedupExtensions(builtinExtensions, installedExtensions, developedExtensions, this._logService); + return dedupExtensions(builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions, this._logService); } private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise { @@ -138,6 +125,19 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return []; } + private async _scanWorkspaceInstalledExtensions(language: string, workspaceInstalledExtensions?: URI[]): Promise { + const result: IExtensionDescription[] = []; + if (workspaceInstalledExtensions?.length) { + const scannedExtensions = await Promise.all(workspaceInstalledExtensions.map(location => this._extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { language }))); + for (const scannedExtension of scannedExtensions) { + if (scannedExtension) { + result.push(toExtensionDescription(scannedExtension, false)); + } + } + } + return result; + } + private async _scanBuiltinExtensions(language: string): Promise { const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); return scannedExtensions.map(e => toExtensionDescription(e, false)); @@ -148,13 +148,6 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return scannedExtensions.map(e => toExtensionDescription(e, false)); } - private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string): Promise { - const extensionLocation = URI.file(resolve(extensionPath)); - const type = isBuiltin ? ExtensionType.System : ExtensionType.User; - const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { language }); - return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; - } - private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise { if ( // No need to install language packs for the default language @@ -319,15 +312,18 @@ export class RemoteExtensionsScannerChannel implements IServerChannel { case 'scanExtensions': { const language = args[0]; const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined; - const extensionDevelopmentPath = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; - const languagePackId: string | undefined = args[3]; - const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath, languagePackId); + const workspaceExtensionLocations = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; + const extensionDevelopmentPath = Array.isArray(args[3]) ? args[3].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; + const languagePackId: string | undefined = args[4]; + const extensions = await this.service.scanExtensions( + language, + profileLocation, + workspaceExtensionLocations, + extensionDevelopmentPath, + languagePackId + ); return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer)); } - case 'scanSingleExtension': { - const extension = await this.service.scanSingleExtension(URI.revive(uriTransformer.transformIncoming(args[0])), args[1], args[2]); - return extension ? transformOutgoingURIs(extension, uriTransformer) : null; - } } throw new Error('Invalid call'); } diff --git a/src/vs/server/node/remoteLanguagePacks.ts b/src/vs/server/node/remoteLanguagePacks.ts index 682b6f2b088..2a1ea9f5699 100644 --- a/src/vs/server/node/remoteLanguagePacks.ts +++ b/src/vs/server/node/remoteLanguagePacks.ts @@ -3,46 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import { FileAccess } from 'vs/base/common/network'; -import * as path from 'vs/base/common/path'; - -import * as lp from 'vs/base/node/languagePacks'; +import { join } from 'vs/base/common/path'; +import type { INLSConfiguration } from 'vs/nls'; +import { resolveNLSConfiguration } from 'vs/base/node/nls'; +import { Promises } from 'vs/base/node/pfs'; import product from 'vs/platform/product/common/product'; -const metaData = path.join(FileAccess.asFileUri('').fsPath, 'nls.metadata.json'); -const _cache: Map> = new Map(); +const nlsMetadataPath = join(FileAccess.asFileUri('').fsPath); +const defaultMessagesFile = join(nlsMetadataPath, 'nls.messages.json'); +const nlsConfigurationCache = new Map>(); -function exists(file: string) { - return new Promise(c => fs.exists(file, c)); -} +export async function getNLSConfiguration(language: string, userDataPath: string): Promise { + if (!product.commit || !(await Promises.exists(defaultMessagesFile))) { + return { + userLocale: 'en', + osLocale: 'en', + resolvedLanguage: 'en', + defaultMessagesFile, -export function getNLSConfiguration(language: string, userDataPath: string): Promise { - return exists(metaData).then((fileExists) => { - if (!fileExists || !product.commit) { - // console.log(`==> MetaData or commit unknown. Using default language.`); - // The OS Locale on the remote side really doesn't matter, so we return the default locale - return Promise.resolve({ locale: 'en', osLocale: 'en', availableLanguages: {} }); - } - const key = `${language}||${userDataPath}`; - let result = _cache.get(key); - if (!result) { - // The OS Locale on the remote side really doesn't matter, so we pass in the same language - result = lp.getNLSConfiguration(product.commit, userDataPath, metaData, language, language).then(value => { - if (InternalNLSConfiguration.is(value)) { - value._languagePackSupport = true; - } - return value; - }); - _cache.set(key, result); - } - return result; - }); -} - -export namespace InternalNLSConfiguration { - export function is(value: lp.NLSConfiguration): value is lp.InternalNLSConfiguration { - const candidate: lp.InternalNLSConfiguration = value as lp.InternalNLSConfiguration; - return candidate && typeof candidate._languagePackId === 'string'; + // NLS: below 2 are a relic from old times only used by vscode-nls and deprecated + locale: 'en', + availableLanguages: {} + }; } + + const cacheKey = `${language}||${userDataPath}`; + let result = nlsConfigurationCache.get(cacheKey); + if (!result) { + result = resolveNLSConfiguration({ userLocale: language, osLocale: language, commit: product.commit, userDataPath, nlsMetadataPath }); + nlsConfigurationCache.set(cacheKey, result); + } + + return result; } diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 6695c4b5a84..fc0f7c259e0 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as _fs from 'fs'; -import * as _url from 'url'; -import * as _cp from 'child_process'; -import * as _http from 'http'; -import * as _os from 'os'; +import * as fs from 'fs'; +import * as url from 'url'; +import * as cp from 'child_process'; +import * as http from 'http'; import { cwd } from 'vs/base/common/process'; import { dirname, extname, resolve, join } from 'vs/base/common/path'; import { parseArgs, buildHelpMessage, buildVersionMessage, OPTIONS, OptionDescriptions, ErrorReporter } from 'vs/platform/environment/node/argv'; @@ -15,6 +14,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { createWaitMarkerFileSync } from 'vs/platform/environment/node/wait'; import { PipeCommand } from 'vs/workbench/api/node/extHostCLIServer'; import { hasStdinWithoutTty, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; +import { DeferredPromise } from 'vs/base/common/async'; /* * Implements a standalone CLI app that opens VS Code from a remote terminal. @@ -181,26 +181,33 @@ export async function main(desc: ProductDescription, args: string[]): Promise | undefined; + if (hasReadStdinArg && hasStdinWithoutTty()) { try { let stdinFilePath = cliStdInFilePath; if (!stdinFilePath) { stdinFilePath = getStdinFilePath(); - await readFromStdin(stdinFilePath, verbose); // throws error if file can not be written + const readFromStdinDone = new DeferredPromise(); + await readFromStdin(stdinFilePath, verbose, () => readFromStdinDone.complete()); // throws error if file can not be written + if (!parsedArgs.wait) { + // if `--wait` is not provided, we keep this process alive + // for at least as long as the stdin stream is open to + // ensure that we read all the data. + readFromStdinPromise = readFromStdinDone.p; + } } // Make sure to open tmp file translatePath(stdinFilePath, mapFileUri, folderURIs, fileURIs); - // Enable --wait to get all data and ignore adding this to history - parsedArgs.wait = true; + // Ignore adding this to history parsedArgs['skip-add-to-recently-opened'] = true; console.log(`Reading from stdin via: ${stdinFilePath}`); } catch (e) { console.log(`Failed to create file to read via stdin: ${e.toString()}`); } - } if (parsedArgs.extensionDevelopmentPath) { @@ -232,8 +239,8 @@ export async function main(desc: ProductDescription, args: string[]): Promise console.log(err)); + const childProcess = cp.fork(join(__dirname, '../../../server-main.js'), cmdLine, { stdio: 'inherit' }); + childProcess.on('error', err => console.log(err)); return; } @@ -262,7 +269,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise process.stdout.write(data)); - cp.stderr.on('data', data => process.stderr.write(data)); + const childProcess = cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: ['inherit', 'pipe', 'pipe'] }); + childProcess.stdout.on('data', data => process.stdout.write(data)); + childProcess.stderr.on('data', data => process.stderr.write(data)); } else { - _cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: 'inherit' }); + cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: 'inherit' }); } } } else { @@ -339,13 +346,17 @@ export async function main(desc: ProductDescription, args: string[]): Promise setTimeout(res, 1000)); } } @@ -364,7 +375,7 @@ function openInBrowser(args: string[], verbose: boolean) { for (const location of args) { try { if (/^(http|https|file):\/\//.test(location)) { - uris.push(_url.parse(location).href); + uris.push(url.parse(location).href); } else { uris.push(pathToURI(location).href); } @@ -394,7 +405,7 @@ function sendToPipe(args: PipeCommand, verbose: boolean): Promise { return; } - const opts: _http.RequestOptions = { + const opts: http.RequestOptions = { socketPath: cliPipe, path: '/', method: 'POST', @@ -404,7 +415,7 @@ function sendToPipe(args: PipeCommand, verbose: boolean): Promise { } }; - const req = _http.request(opts, res => { + const req = http.request(opts, res => { if (res.headers['content-type'] !== 'application/json') { reject('Error in response: Invalid content type: Expected \'application/json\', is: ' + res.headers['content-type']); return; @@ -449,18 +460,18 @@ function fatal(message: string, err: any): void { const preferredCwd = process.env.PWD || cwd(); // prefer process.env.PWD as it does not follow symlinks -function pathToURI(input: string): _url.URL { +function pathToURI(input: string): url.URL { input = input.trim(); input = resolve(preferredCwd, input); - return _url.pathToFileURL(input); + return url.pathToFileURL(input); } function translatePath(input: string, mapFileUri: (input: string) => string, folderURIS: string[], fileURIS: string[]) { const url = pathToURI(input); const mappedUri = mapFileUri(url.href); try { - const stat = _fs.lstatSync(_fs.realpathSync(input)); + const stat = fs.lstatSync(fs.realpathSync(input)); if (stat.isFile()) { fileURIS.push(mappedUri); diff --git a/src/vs/server/node/serverConnectionToken.ts b/src/vs/server/node/serverConnectionToken.ts index f976d0e379a..6a6bdcdb56a 100644 --- a/src/vs/server/node/serverConnectionToken.ts +++ b/src/vs/server/node/serverConnectionToken.ts @@ -100,7 +100,7 @@ export async function determineServerConnectionToken(args: ServerParsedArgs): Pr // First try to find a connection token try { - const fileContents = await Promises.readFile(storageLocation); + const fileContents = await fs.promises.readFile(storageLocation); const connectionToken = fileContents.toString().replace(/\r?\n$/, ''); if (connectionTokenRegex.test(connectionToken)) { return connectionToken; diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 6c7a0d7901d..46f2f004655 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -9,7 +9,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { IURITransformer } from 'vs/base/common/uriIpc'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { Promises } from 'vs/base/node/pfs'; import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; @@ -132,11 +132,12 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel('userDataProfiles', new RemoteUserDataProfilesServiceChannel(userDataProfilesService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); // Initialize - const [, , machineId, sqmId] = await Promise.all([ + const [, , machineId, sqmId, devDeviceId] = await Promise.all([ configurationService.initialize(), userDataProfilesService.init(), getMachineId(logService.error.bind(logService)), - getSqmMachineId(logService.error.bind(logService)) + getSqmMachineId(logService.error.bind(logService)), + getdevDeviceId(logService.error.bind(logService)) ]); const extensionHostStatusService = new ExtensionHostStatusService(); @@ -156,7 +157,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const config: ITelemetryServiceConfig = { appenders: [oneDsAppender], - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, isInternal, 'remoteAgent'), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, devDeviceId, isInternal, 'remoteAgent'), piiPaths: getPiiPathsFromEnvironment(environmentService) }; const initialTelemetryLevelArg = environmentService.args['telemetry-level']; @@ -216,7 +217,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI, logService), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService); socketServer.registerChannel(RemoteExtensionsScannerChannelName, new RemoteExtensionsScannerChannel(remoteExtensionsScanner, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); - const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(logService, environmentService); + const remoteFileSystemChannel = disposables.add(new RemoteAgentFileSystemProviderChannel(logService, environmentService)); socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); socketServer.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index f0c43c66aef..2abd1e1366c 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createReadStream } from 'fs'; -import { Promises } from 'vs/base/node/pfs'; +import { createReadStream, promises } from 'fs'; import * as path from 'path'; import * as http from 'http'; import * as url from 'url'; @@ -30,13 +29,13 @@ import { isString } from 'vs/base/common/types'; import { CharCode } from 'vs/base/common/charCode'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -const textMimeType = { +const textMimeType: { [ext: string]: string | undefined } = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.svg': 'image/svg+xml', -} as { [ext: string]: string | undefined }; +}; /** * Return an error to the client. @@ -55,7 +54,7 @@ export const enum CacheControl { */ export async function serveFile(filePath: string, cacheControl: CacheControl, logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, responseHeaders: Record): Promise { try { - const stat = await Promises.stat(filePath); // throws an error if file doesn't exist + const stat = await promises.stat(filePath); // throws an error if file doesn't exist if (cacheControl === CacheControl.ETAG) { // Check if file modified since @@ -222,7 +221,7 @@ export class WebClientServer { return serveError(req, res, status, text || `Request failed with status ${status}`); } - const responseHeaders: Record = Object.create(null); + const responseHeaders: Record = Object.create(null); const setResponseHeader = (header: string) => { const value = context.res.headers[header]; if (value) { @@ -306,21 +305,21 @@ export class WebClientServer { scopes: [['user:email'], ['repo']] } : undefined; - const productConfiguration = >{ + const productConfiguration = { embedderIdentifier: 'server-distro', - extensionsGallery: this._webExtensionResourceUrlTemplate ? { + extensionsGallery: this._webExtensionResourceUrlTemplate && this._productService.extensionsGallery ? { ...this._productService.extensionsGallery, - 'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({ + resourceUrlTemplate: this._webExtensionResourceUrlTemplate.with({ scheme: 'http', authority: remoteAuthority, path: `${this._webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` }).toString(true) } : undefined - }; + } satisfies Partial; if (!this._environmentService.isBuilt) { try { - const productOverrides = JSON.parse((await Promises.readFile(join(APP_ROOT, 'product.overrides.json'))).toString()); + const productOverrides = JSON.parse((await promises.readFile(join(APP_ROOT, 'product.overrides.json'))).toString()); Object.assign(productConfiguration, productOverrides); } catch (err) {/* Ignore Error */ } } @@ -338,18 +337,29 @@ export class WebClientServer { callbackRoute: this._callbackRoute }; - const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl; + const cookies = cookie.parse(req.headers.cookie || ''); + const locale = cookies['vscode.nls.locale'] || req.headers['accept-language']?.split(',')[0]?.toLowerCase() || 'en'; + let WORKBENCH_NLS_BASE_URL: string | undefined; + let WORKBENCH_NLS_URL: string; + if (!locale.startsWith('en') && this._productService.nlsCoreBaseUrl) { + WORKBENCH_NLS_BASE_URL = this._productService.nlsCoreBaseUrl; + WORKBENCH_NLS_URL = `${WORKBENCH_NLS_BASE_URL}${this._productService.commit}/${this._productService.version}/${locale}/nls.messages.js`; + } else { + WORKBENCH_NLS_URL = ''; // fallback will apply + } + const values: { [key: string]: string } = { WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '', WORKBENCH_WEB_BASE_URL: this._staticRoute, - WORKBENCH_NLS_BASE_URL: nlsBaseUrl ? `${nlsBaseUrl}${!nlsBaseUrl.endsWith('/') ? '/' : ''}${this._productService.commit}/${this._productService.version}/` : '', + WORKBENCH_NLS_URL, + WORKBENCH_NLS_FALLBACK_URL: `${this._staticRoute}/out/nls.messages.js` }; if (useTestResolver) { const bundledExtensions: { extensionPath: string; packageJSON: IExtensionManifest }[] = []; for (const extensionPath of ['vscode-test-resolver', 'github-authentication']) { - const packageJSON = JSON.parse((await Promises.readFile(FileAccess.asFileUri(`${builtinExtensionsPath}/${extensionPath}/package.json`).fsPath)).toString()); + const packageJSON = JSON.parse((await promises.readFile(FileAccess.asFileUri(`${builtinExtensionsPath}/${extensionPath}/package.json`).fsPath)).toString()); bundledExtensions.push({ extensionPath, packageJSON }); } values['WORKBENCH_BUILTIN_EXTENSIONS'] = asJSON(bundledExtensions); @@ -357,20 +367,20 @@ export class WebClientServer { let data; try { - const workbenchTemplate = (await Promises.readFile(filePath)).toString(); + const workbenchTemplate = (await promises.readFile(filePath)).toString(); data = workbenchTemplate.replace(/\{\{([^}]+)\}\}/g, (_, key) => values[key] ?? 'undefined'); } catch (e) { res.writeHead(404, { 'Content-Type': 'text/plain' }); return void res.end('Not found'); } - const webWorkerExtensionHostIframeScriptSHA = 'sha256-75NYUUvf+5++1WbfCZOV3PSWxBhONpaxwx+mkOFRv/Y='; + const webWorkerExtensionHostIframeScriptSHA = 'sha256-V28GQnL3aYxbwgpV3yW1oJ+VKKe/PBSzWntNyH8zVXA='; const cspDirectives = [ 'default-src \'self\';', 'img-src \'self\' https: data: blob:;', 'media-src \'self\';', - `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html + `script-src 'self' 'unsafe-eval' ${WORKBENCH_NLS_BASE_URL ?? ''} ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html 'child-src \'self\';', `frame-src 'self' https://*.vscode-cdn.net data:;`, 'worker-src \'self\' data: blob:;', @@ -426,7 +436,7 @@ export class WebClientServer { */ private async _handleCallback(res: http.ServerResponse): Promise { const filePath = FileAccess.asFileUri('vs/code/browser/workbench/callback.html').fsPath; - const data = (await Promises.readFile(filePath)).toString(); + const data = (await promises.readFile(filePath)).toString(); const cspDirectives = [ 'default-src \'self\';', 'img-src \'self\' https: data: blob:;', diff --git a/src/vs/server/test/node/serverConnectionToken.test.ts b/src/vs/server/test/node/serverConnectionToken.test.ts index b624dd6d676..b04dab4d308 100644 --- a/src/vs/server/test/node/serverConnectionToken.test.ts +++ b/src/vs/server/test/node/serverConnectionToken.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 1fc190391ff..1d563ea1dce 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -20,6 +20,8 @@ import './mainThreadBulkEdits'; import './mainThreadLanguageModels'; import './mainThreadChatAgents2'; import './mainThreadChatVariables'; +import './mainThreadLanguageModelTools'; +import './mainThreadEmbeddings'; import './mainThreadCodeInsets'; import './mainThreadCLICommands'; import './mainThreadClipboard'; @@ -75,8 +77,6 @@ import './mainThreadNotebookDocumentsAndEditors'; import './mainThreadNotebookRenderers'; import './mainThreadNotebookSaveParticipant'; import './mainThreadInteractive'; -import './mainThreadInlineChat'; -import './mainThreadChat'; import './mainThreadTask'; import './mainThreadLabelService'; import './mainThreadTunnelService'; @@ -88,7 +88,6 @@ import './mainThreadShare'; import './mainThreadProfileContentHandlers'; import './mainThreadAiRelatedInformation'; import './mainThreadAiEmbeddingVector'; -import './mainThreadIssueReporter'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index b3ebdd940c3..c1500981d23 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,7 +6,7 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -19,6 +19,7 @@ import { IAuthenticationUsageService } from 'vs/workbench/services/authenticatio import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { CancellationError } from 'vs/base/common/errors'; interface AuthenticationForceNewSessionOptions { detail?: string; @@ -31,6 +32,7 @@ interface AuthenticationGetSessionOptions { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; silent?: boolean; + account?: AuthenticationSessionAccount; } export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -49,8 +51,8 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut this.onDidChangeSessions = onDidChangeSessionsEmitter.event; } - async getSessions(scopes?: string[]) { - return this._proxy.$getSessions(this.id, scopes); + async getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions) { + return this._proxy.$getSessions(this.id, scopes, options); } createSession(scopes: string[], options: IAuthenticationCreateSessionOptions): Promise { @@ -117,10 +119,17 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu $removeSession(providerId: string, sessionId: string): Promise { return this.authenticationService.removeSession(providerId, sessionId); } - private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { - const message = recreatingSession - ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName) - : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName); + private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { + let message: string; + + // An internal provider is a special case which is for model access only. + if (provider.id.startsWith(INTERNAL_MODEL_AUTH_PROVIDER_PREFIX)) { + message = nls.localize('confirmModelAccess', "The extension '{0}' wants to access the language models provided by {1}.", extensionName, provider.label); + } else { + message = recreatingSession + ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, provider.label) + : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, provider.label); + } const buttons: IPromptButton[] = [ { @@ -134,7 +143,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu buttons.push({ label: nls.localize('learnMore', "Learn more"), run: async () => { - const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + const result = this.loginPrompt(provider, extensionName, recreatingSession, options); await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); return await result; } @@ -151,8 +160,33 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return result ?? false; } + private async continueWithIncorrectAccountPrompt(chosenAccountLabel: string, requestedAccountLabel: string): Promise { + const result = await this.dialogService.prompt({ + message: nls.localize('incorrectAccount', "Incorrect account detected"), + detail: nls.localize('incorrectAccountDetail', "The chosen account, {0}, does not match the requested account, {1}.", chosenAccountLabel, requestedAccountLabel), + type: Severity.Warning, + cancelButton: true, + buttons: [ + { + label: nls.localize('keep', 'Keep {0}', chosenAccountLabel), + run: () => chosenAccountLabel + }, + { + label: nls.localize('loginWith', 'Login with {0}', requestedAccountLabel), + run: () => requestedAccountLabel + } + ], + }); + + if (!result.result) { + throw new CancellationError(); + } + + return result.result === chosenAccountLabel; + } + private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { - const sessions = await this.authenticationService.getSessions(providerId, scopes, true); + const sessions = await this.authenticationService.getSessions(providerId, scopes, options.account, true); const provider = this.authenticationService.getProvider(providerId); // Error cases @@ -166,21 +200,21 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent'); } + if (options.clearSessionPreference) { + // Clearing the session preference is usually paired with createIfNone, so just remove the preference and + // defer to the rest of the logic in this function to choose the session. + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); + } + // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { if (provider.supportsMultipleAccounts) { - if (options.clearSessionPreference) { - // Clearing the session preference is usually paired with createIfNone, so just remove the preference and - // defer to the rest of the logic in this function to choose the session. - this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); - } else { - // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - if (existingSessionPreference) { - const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { - return matchingSession; - } + // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); + if (existingSessionPreference) { + const matchingSession = sessions.find(session => session.id === existingSessionPreference); + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + return matchingSession; } } } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { @@ -199,25 +233,30 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); + const isAllowed = await this.loginPrompt(provider, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } - let session; + let session: AuthenticationSession; if (sessions?.length && !options.forceNewSession) { - session = provider.supportsMultipleAccounts + session = provider.supportsMultipleAccounts && !options.account ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { - let sessionToRecreate: AuthenticationSession | undefined; - if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { - sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; - } else { + let accountToCreate: AuthenticationSessionAccount | undefined = options.account; + if (!accountToCreate) { const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; + accountToCreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate)?.account : undefined; } - session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); + + do { + session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, account: accountToCreate }); + } while ( + accountToCreate + && accountToCreate.label !== session.account.label + && !await this.continueWithIncorrectAccountPrompt(session.account.label, accountToCreate.label) + ); } this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); @@ -254,16 +293,9 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return session; } - async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { - const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); - if (accessibleSessions.length) { - this.sendProviderUsageTelemetry(extensionId, providerId); - for (const session of accessibleSessions) { - this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); - } - } - return accessibleSessions; + async $getAccounts(providerId: string): Promise> { + const accounts = await this.authenticationService.getAccounts(providerId); + return accounts; } private sendProviderUsageTelemetry(extensionId: string, providerId: string): void { diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts index 6a42b3b1050..dfd425729a3 100644 --- a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -9,9 +9,11 @@ import { IBulkEditService, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/ import { WorkspaceEdit } from 'vs/editor/common/languages'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IWorkspaceEditDto, IWorkspaceFileEditDto, MainContext, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IWorkspaceCellEditDto, IWorkspaceEditDto, IWorkspaceFileEditDto, MainContext, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @extHostNamedCustomer(MainContext.MainThreadBulkEdits) @@ -26,8 +28,8 @@ export class MainThreadBulkEdits implements MainThreadBulkEditsShape { dispose(): void { } - $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto, undoRedoGroupId?: number, isRefactoring?: boolean): Promise { - const edits = reviveWorkspaceEditDto(dto, this._uriIdentService); + $tryApplyWorkspaceEdit(dto: SerializableObjectWithBuffers, undoRedoGroupId?: number, isRefactoring?: boolean): Promise { + const edits = reviveWorkspaceEditDto(dto.value, this._uriIdentService); return this._bulkEditService.apply(edits, { undoRedoGroupId, respectAutoSaveConfig: isRefactoring }).then((res) => res.isApplied, err => { this._logService.warn(`IGNORING workspace edit: ${err}`); return false; @@ -66,6 +68,24 @@ export function reviveWorkspaceEditDto(data: IWorkspaceEditDto | undefined, uriI } if (ResourceNotebookCellEdit.is(edit)) { edit.resource = uriIdentityService.asCanonicalUri(edit.resource); + const cellEdit = (edit as IWorkspaceCellEditDto).cellEdit; + if (cellEdit.editType === CellEditType.Replace) { + edit.cellEdit = { + ...cellEdit, + cells: cellEdit.cells.map(cell => ({ + ...cell, + outputs: cell.outputs.map(output => ({ + ...output, + outputs: output.items.map(item => { + return { + mime: item.mime, + data: item.valueBytes + }; + }) + })) + })) + }; + } } } return data; diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts deleted file mode 100644 index 1a4d657cd94..00000000000 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ /dev/null @@ -1,81 +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 } from 'vs/base/common/event'; -import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { ExtHostChatShape, ExtHostContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; - -@extHostNamedCustomer(MainContext.MainThreadChat) -export class MainThreadChat extends Disposable implements MainThreadChatShape { - - private readonly _providerRegistrations = this._register(new DisposableMap()); - private readonly _stateEmitters = new Map>(); - - private readonly _proxy: ExtHostChatShape; - - constructor( - extHostContext: IExtHostContext, - @IChatService private readonly _chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - @IChatContributionService private readonly chatContribService: IChatContributionService, - ) { - super(); - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChat); - } - - $transferChatSession(sessionId: number, toWorkspace: UriComponents): void { - const sessionIdStr = this._chatService.getSessionId(sessionId); - if (!sessionIdStr) { - throw new Error(`Failed to transfer session. Unknown session provider ID: ${sessionId}`); - } - - const widget = this._chatWidgetService.getWidgetBySessionId(sessionIdStr); - const inputValue = widget?.inputEditor.getValue() ?? ''; - this._chatService.transferChatSession({ sessionId: sessionIdStr, inputValue: inputValue }, URI.revive(toWorkspace)); - } - - async $registerChatProvider(handle: number, id: string): Promise { - const registration = this.chatContribService.registeredProviders.find(staticProvider => staticProvider.id === id); - if (!registration) { - throw new Error(`Provider ${id} must be declared in the package.json.`); - } - - const unreg = this._chatService.registerProvider({ - id, - prepareSession: async (token) => { - const session = await this._proxy.$prepareChat(handle, token); - if (!session) { - return undefined; - } - - const emitter = new Emitter(); - this._stateEmitters.set(session.id, emitter); - return { - id: session.id, - dispose: () => { - emitter.dispose(); - this._stateEmitters.delete(session.id); - this._proxy.$releaseSession(session.id); - } - }; - }, - }); - - this._providerRegistrations.set(handle, unreg); - } - - async $acceptChatState(sessionId: number, state: any): Promise { - this._stateEmitters.get(sessionId)?.fire(state); - } - - async $unregisterChatProvider(handle: number): Promise { - this._providerRegistrations.deleteAndDispose(handle); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 0aaaee8cf14..3a403ad039c 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -3,10 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { getWordAtText } from 'vs/editor/common/core/wordHelper'; @@ -15,15 +20,17 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtHostChatAgentsShape2, ExtHostContext, IChatParticipantMetadata, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatContentReference, IChatFollowup, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; interface AgentData { dispose: () => void; @@ -32,15 +39,51 @@ interface AgentData { hasFollowups?: boolean; } +class MainThreadChatTask implements IChatTask { + public readonly kind = 'progressTask'; + + public readonly deferred = new DeferredPromise(); + + private readonly _onDidAddProgress = new Emitter(); + public get onDidAddProgress(): Event { return this._onDidAddProgress.event; } + + public readonly progress: (IChatWarningMessage | IChatContentReference)[] = []; + + constructor(public content: IMarkdownString) { } + + task() { + return this.deferred.p; + } + + isSettled() { + return this.deferred.isSettled; + } + + complete(v: string | void) { + this.deferred.complete(v); + } + + add(progress: IChatWarningMessage | IChatContentReference): void { + this.progress.push(progress); + this._onDidAddProgress.fire(progress); + } +} + @extHostNamedCustomer(MainContext.MainThreadChatAgents2) export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 { private readonly _agents = this._register(new DisposableMap()); private readonly _agentCompletionProviders = this._register(new DisposableMap()); + private readonly _agentIdsToCompletionProviders = this._register(new DisposableMap); + + private readonly _chatParticipantDetectionProviders = this._register(new DisposableMap()); private readonly _pendingProgress = new Map void>(); private readonly _proxy: ExtHostChatAgentsShape2; + private _responsePartHandlePool = 0; + private readonly _activeTasks = new Map(); + constructor( extHostContext: IExtHostContext, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @@ -48,6 +91,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + @IExtensionService private readonly _extensionService: IExtensionService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -75,7 +120,19 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.deleteAndDispose(handle); } - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void { + $transferActiveChatSession(toWorkspace: UriComponents): void { + const widget = this._chatWidgetService.lastFocusedWidget; + const sessionId = widget?.viewModel?.model.sessionId; + if (!sessionId) { + this._logService.error(`MainThreadChat#$transferActiveChatSession: No active chat session found`); + return; + } + + const inputValue = widget?.inputEditor.getValue() ?? ''; + this._chatService.transferChatSession({ sessionId, inputValue }, URI.revive(toWorkspace)); + } + + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void { const staticAgentRegistration = this._chatAgentService.getAgent(id); if (!staticAgentRegistration && !dynamicProps) { if (this._chatAgentService.getAgentsByName(id).length) { @@ -103,22 +160,27 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return this._proxy.$provideFollowups(request, handle, result, { history }, token); }, - provideWelcomeMessage: (token: CancellationToken) => { - return this._proxy.$provideWelcomeMessage(handle, token); + provideWelcomeMessage: (location: ChatAgentLocation, token: CancellationToken) => { + return this._proxy.$provideWelcomeMessage(handle, location, token); }, - provideSampleQuestions: (token: CancellationToken) => { - return this._proxy.$provideSampleQuestions(handle, token); + provideSampleQuestions: (location: ChatAgentLocation, token: CancellationToken) => { + return this._proxy.$provideSampleQuestions(handle, location, token); } }; let disposable: IDisposable; if (!staticAgentRegistration && dynamicProps) { + const extensionDescription = this._extensionService.extensions.find(e => ExtensionIdentifier.equals(e.identifier, extension)); disposable = this._chatAgentService.registerDynamicAgent( { id, name: dynamicProps.name, description: dynamicProps.description, extensionId: extension, + extensionDisplayName: extensionDescription?.displayName ?? extension.value, + extensionPublisherId: extensionDescription?.publisher ?? '', + publisherDisplayName: dynamicProps.publisherName, + fullName: dynamicProps.fullName, metadata: revive(metadata), slashCommands: [], locations: [ChatAgentLocation.Panel] // TODO all dynamic participants are panel only? @@ -139,18 +201,50 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void { const data = this._agents.get(handle); if (!data) { - throw new Error(`No agent with handle ${handle} registered`); + this._logService.error(`MainThreadChatAgents2#$updateAgent: No agent with handle ${handle} registered`); + return; } data.hasFollowups = metadataUpdate.hasFollowups; this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); } - async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise { - const revivedProgress = revive(progress); - this._pendingProgress.get(requestId)?.(revivedProgress as IChatProgress); + async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { + const revivedProgress = revive(progress) as IChatProgress; + if (revivedProgress.kind === 'progressTask') { + const handle = ++this._responsePartHandlePool; + const responsePartId = `${requestId}_${handle}`; + const task = new MainThreadChatTask(revivedProgress.content); + this._activeTasks.set(responsePartId, task); + this._pendingProgress.get(requestId)?.(task); + return handle; + } else if (responsePartHandle !== undefined) { + const responsePartId = `${requestId}_${responsePartHandle}`; + const task = this._activeTasks.get(responsePartId); + switch (revivedProgress.kind) { + case 'progressTaskResult': + if (task && revivedProgress.content) { + task.complete(revivedProgress.content.value); + this._activeTasks.delete(responsePartId); + } else { + task?.complete(undefined); + } + return responsePartHandle; + case 'warning': + case 'reference': + task?.add(revivedProgress); + return; + } + } + this._pendingProgress.get(requestId)?.(revivedProgress); } - $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void { + $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void { + const provide = async (query: string, token: CancellationToken) => { + const completions = await this._proxy.$invokeCompletionProvider(handle, query, token); + return completions.map((c) => ({ ...c, icon: c.icon ? ThemeIcon.fromId(c.icon) : undefined })); + }; + this._agentIdsToCompletionProviders.set(id, this._chatAgentService.registerAgentCompletionProvider(id, provide)); + this._agentCompletionProviders.set(handle, this._languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'chatAgentCompletions:' + handle, triggerCharacters, @@ -180,7 +274,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return null; } - const result = await this._proxy.$invokeCompletionProvider(handle, query, token); + const result = await provide(query, token); const variableItems = result.map(v => { const insertText = v.insertText ?? (typeof v.label === 'string' ? v.label : v.label.label); const rangeAfterInsert = new Range(range.insert.startLineNumber, range.insert.startColumn, range.insert.endLineNumber, range.insert.startColumn + insertText.length); @@ -191,7 +285,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA kind: CompletionItemKind.Text, detail: v.detail, documentation: v.documentation, - command: { id: AddDynamicVariableAction.ID, title: '', arguments: [{ widget, range: rangeAfterInsert, variableData: revive(v.values) } satisfies IAddDynamicVariableContext] } + command: { id: AddDynamicVariableAction.ID, title: '', arguments: [{ id: v.id, widget, range: rangeAfterInsert, variableData: revive(v.value) as any, command: v.command } satisfies IAddDynamicVariableContext] } } satisfies CompletionItem; }); @@ -202,8 +296,23 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA })); } - $unregisterAgentCompletionsProvider(handle: number): void { + $unregisterAgentCompletionsProvider(handle: number, id: string): void { this._agentCompletionProviders.deleteAndDispose(handle); + this._agentIdsToCompletionProviders.deleteAndDispose(id); + } + + $registerChatParticipantDetectionProvider(handle: number): void { + this._chatParticipantDetectionProviders.set(handle, this._chatAgentService.registerChatParticipantDetectionProvider(handle, + { + provideParticipantDetection: async (request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation; participants: IChatParticipantMetadata[] }, token: CancellationToken) => { + return await this._proxy.$detectChatParticipant(handle, request, { history }, options, token); + } + } + )); + } + + $unregisterChatParticipantDetectionProvider(handle: number): void { + this._chatParticipantDetectionProviders.deleteAndDispose(handle); } } diff --git a/src/vs/workbench/api/browser/mainThreadChatVariables.ts b/src/vs/workbench/api/browser/mainThreadChatVariables.ts index fbdc3060424..9e08e5d1423 100644 --- a/src/vs/workbench/api/browser/mainThreadChatVariables.ts +++ b/src/vs/workbench/api/browser/mainThreadChatVariables.ts @@ -31,10 +31,10 @@ export class MainThreadChatVariables implements MainThreadChatVariablesShape { const registration = this._chatVariablesService.registerVariable(data, async (messageText, _arg, model, progress, token) => { const varRequestId = `${model.sessionId}-${handle}`; this._pendingProgress.set(varRequestId, progress); - const result = revive(await this._proxy.$resolveVariable(handle, varRequestId, messageText, token)); + const result = revive(await this._proxy.$resolveVariable(handle, varRequestId, messageText, token)); this._pendingProgress.delete(varRequestId); - return result; + return result as any; // 'revive' type signature doesn't like this type for some reason }); this._variables.set(handle, registration); } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 5bfadbbc409..1504c0d2029 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -12,7 +12,7 @@ import * as languages from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ICommentController, ICommentInfo, ICommentService, INotebookCommentInfo } from 'vs/workbench/contrib/comments/browser/commentService'; +import { ICommentController, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; import { CommentProviderFeatures, ExtHostCommentsShape, ExtHostContext, MainContext, MainThreadCommentsShape, CommentThreadChanges } from '../common/extHost.protocol'; import { COMMENTS_VIEW_ID, COMMENTS_VIEW_STORAGE_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; @@ -27,6 +27,9 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { Schemas } from 'vs/base/common/network'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { MarshalledCommentThread } from 'vs/workbench/common/comments'; +import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; export class MainThreadCommentThread implements languages.CommentThread { private _input?: languages.CommentInput; @@ -66,13 +69,13 @@ export class MainThreadCommentThread implements languages.CommentThread { private readonly _onDidChangeLabel = new Emitter(); readonly onDidChangeLabel: Event = this._onDidChangeLabel.event; - private _comments: languages.Comment[] | undefined; + private _comments: ReadonlyArray | undefined; - public get comments(): languages.Comment[] | undefined { + public get comments(): ReadonlyArray | undefined { return this._comments; } - public set comments(newComments: languages.Comment[] | undefined) { + public set comments(newComments: ReadonlyArray | undefined) { this._comments = newComments; this._onDidChangeComments.fire(this._comments); } @@ -109,8 +112,10 @@ export class MainThreadCommentThread implements languages.CommentThread { } set collapsibleState(newState: languages.CommentThreadCollapsibleState | undefined) { - this._collapsibleState = newState; - this._onDidChangeCollapsibleState.fire(this._collapsibleState); + if (newState !== this._collapsibleState) { + this._collapsibleState = newState; + this._onDidChangeCollapsibleState.fire(this._collapsibleState); + } } private _initialCollapsibleState: languages.CommentThreadCollapsibleState | undefined; @@ -179,12 +184,16 @@ export class MainThreadCommentThread implements languages.CommentThread { public threadId: string, public resource: string, private _range: T | undefined, + comments: languages.Comment[] | undefined, private _canReply: boolean, - private _isTemplate: boolean + private _isTemplate: boolean, + public editorId?: string ) { this._isDisposed = false; if (_isTemplate) { this.comments = []; + } else if (comments) { + this._comments = comments; } } @@ -195,7 +204,7 @@ export class MainThreadCommentThread implements languages.CommentThread { if (modified('range')) { this._range = changes.range!; } if (modified('label')) { this._label = changes.label; } if (modified('contextValue')) { this._contextValue = changes.contextValue === null ? undefined : changes.contextValue; } - if (modified('comments')) { this._comments = changes.comments; } + if (modified('comments')) { this.comments = changes.comments; } if (modified('collapseState')) { this.initialCollapsibleState = changes.collapseState; } if (modified('canReply')) { this.canReply = changes.canReply!; } if (modified('state')) { this.state = changes.state!; } @@ -203,6 +212,10 @@ export class MainThreadCommentThread implements languages.CommentThread { if (modified('isTemplate')) { this._isTemplate = changes.isTemplate!; } } + hasComments(): boolean { + return !!this.comments && this.comments.length > 0; + } + dispose() { this._isDisposed = true; this._onDidChangeCollapsibleState.dispose(); @@ -291,7 +304,9 @@ export class MainThreadCommentController implements ICommentController { threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, - isTemplate: boolean + comments: languages.Comment[], + isTemplate: boolean, + editorId?: string ): languages.CommentThread { const thread = new MainThreadCommentThread( commentThreadHandle, @@ -300,8 +315,10 @@ export class MainThreadCommentController implements ICommentController { threadId, URI.revive(resource).toString(), range, + comments, true, - isTemplate + isTemplate, + editorId ); this._threads.set(commentThreadHandle, thread); @@ -416,46 +433,50 @@ export class MainThreadCommentController implements ICommentController { }; } - const ret: languages.CommentThread[] = []; + const ret: languages.CommentThread[] = []; for (const thread of [...this._threads.keys()]) { const commentThread = this._threads.get(thread)!; if (commentThread.resource === resource.toString()) { - ret.push(commentThread); + if (commentThread.isDocumentCommentThread()) { + ret.push(commentThread); + } } } const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); - return { + return { uniqueOwner: this._uniqueId, label: this.label, threads: ret, commentingRanges: { resource: resource, ranges: commentingRanges?.ranges || [], - fileComments: commentingRanges?.fileComments + fileComments: !!commentingRanges?.fileComments } }; } async getNotebookComments(resource: URI, token: CancellationToken) { if (resource.scheme !== Schemas.vscodeNotebookCell) { - return { + return { uniqueOwner: this._uniqueId, label: this.label, threads: [] }; } - const ret: languages.CommentThread[] = []; + const ret: languages.CommentThread[] = []; for (const thread of [...this._threads.keys()]) { const commentThread = this._threads.get(thread)!; if (commentThread.resource === resource.toString()) { - ret.push(commentThread); + if (!commentThread.isDocumentCommentThread()) { + ret.push(commentThread as languages.CommentThread); + } } } - return { + return { uniqueOwner: this._uniqueId, label: this.label, threads: ret @@ -475,8 +496,8 @@ export class MainThreadCommentController implements ICommentController { return ret; } - createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise { - return this._proxy.$createCommentThreadTemplate(this.handle, resource, range); + createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined, editorId?: string): Promise { + return this._proxy.$createCommentThreadTemplate(this.handle, resource, range, editorId); } async updateCommentThreadTemplate(threadHandle: number, range: IRange) { @@ -511,7 +532,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments extHostContext: IExtHostContext, @ICommentService private readonly _commentService: ICommentService, @IViewsService private readonly _viewsService: IViewsService, - @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, + @IEditorService private readonly _editorService: IEditorService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); @@ -575,8 +598,10 @@ export class MainThreadComments extends Disposable implements MainThreadComments threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, + comments: languages.Comment[], extensionId: ExtensionIdentifier, - isTemplate: boolean + isTemplate: boolean, + editorId?: string ): languages.CommentThread | undefined { const provider = this._commentControllers.get(handle); @@ -584,7 +609,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, isTemplate); + return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, comments, isTemplate, editorId); } $updateCommentThread(handle: number, @@ -621,6 +646,38 @@ export class MainThreadComments extends Disposable implements MainThreadComments provider.updateCommentingRanges(resourceHints); } + async $revealCommentThread(handle: number, commentThreadHandle: number, commentUniqueIdInThread: number, options: languages.CommentThreadRevealOptions): Promise { + const provider = this._commentControllers.get(handle); + + if (!provider) { + return Promise.resolve(); + } + + const thread = provider.getAllComments().find(thread => thread.commentThreadHandle === commentThreadHandle); + if (!thread || !thread.isDocumentCommentThread()) { + return Promise.resolve(); + } + + const comment = thread.comments?.find(comment => comment.uniqueIdInThread === commentUniqueIdInThread); + + revealCommentThread(this._commentService, this._editorService, this._uriIdentityService, thread, comment, options.focusReply, undefined, options.preserveFocus); + } + + async $hideCommentThread(handle: number, commentThreadHandle: number): Promise { + const provider = this._commentControllers.get(handle); + + if (!provider) { + return Promise.resolve(); + } + + const thread = provider.getAllComments().find(thread => thread.commentThreadHandle === commentThreadHandle); + if (!thread || !thread.isDocumentCommentThread()) { + return Promise.resolve(); + } + + thread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; + } + private registerView(commentsViewAlreadyRegistered: boolean) { if (!commentsViewAlreadyRegistered) { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index e5dfc1a814e..36b5a4df0ae 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -19,6 +19,7 @@ import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Event } from 'vs/base/common/event'; +import { isDefined } from 'vs/base/common/types'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -210,21 +211,26 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb for (const dto of DTOs) { if (dto.type === 'sourceMulti') { - const rawbps = dto.lines.map(l => - { - id: l.id, - enabled: l.enabled, - lineNumber: l.line + 1, - column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784 - condition: l.condition, - hitCondition: l.hitCondition, - logMessage: l.logMessage, - mode: l.mode, - } - ); + const rawbps = dto.lines.map((l): IBreakpointData => ({ + id: l.id, + enabled: l.enabled, + lineNumber: l.line + 1, + column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784 + condition: l.condition, + hitCondition: l.hitCondition, + logMessage: l.logMessage, + mode: l.mode, + })); this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps); } else if (dto.type === 'function') { - this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); + this.debugService.addFunctionBreakpoint({ + name: dto.functionName, + mode: dto.mode, + condition: dto.condition, + hitCondition: dto.hitCondition, + enabled: dto.enabled, + logMessage: dto.logMessage + }, dto.id); } else if (dto.type === 'data') { this.debugService.addDataBreakpoint({ description: dto.label, @@ -248,7 +254,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $registerDebugConfigurationProvider(debugType: string, providerTriggerKind: DebugConfigurationProviderTriggerKind, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, handle: number): Promise { - const provider = { + const provider: IDebugConfigurationProvider = { type: debugType, triggerKind: providerTriggerKind }; @@ -283,7 +289,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $registerDebugAdapterDescriptorFactory(debugType: string, handle: number): Promise { - const provider = { + const provider: IDebugAdapterDescriptorFactory = { type: debugType, createDebugAdapterDescriptor: session => { return Promise.resolve(this._proxy.$provideDebugAdapter(handle, this.getSessionDto(session))); @@ -323,6 +329,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb compact: options.compact, compoundRoot: parentSession?.compoundRoot, saveBeforeRestart: saveBeforeStart, + testRun: options.testRun, suppressDebugStatusbar: options.suppressDebugStatusbar, suppressDebugToolbar: options.suppressDebugToolbar, @@ -385,7 +392,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } public $acceptDAError(handle: number, name: string, message: string, stack: string) { - this.getDebugAdapter(handle).fireError(handle, new Error(`${name}: ${message}\n${stack}`)); + // don't use getDebugAdapter since an error can be expected on a post-close + this._debugAdapters.get(handle)?.fireError(handle, new Error(`${name}: ${message}\n${stack}`)); } public $acceptDAExit(handle: number, code: number, signal: string) { @@ -434,8 +442,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private convertToDto(bps: (ReadonlyArray)): Array { return bps.map(bp => { if ('name' in bp) { - const fbp = bp; - return { + const fbp: IFunctionBreakpoint = bp; + return { type: 'function', id: fbp.getId(), enabled: fbp.enabled, @@ -443,9 +451,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb hitCondition: fbp.hitCondition, logMessage: fbp.logMessage, functionName: fbp.name - }; + } satisfies IFunctionBreakpointDto; } else if ('src' in bp) { - const dbp = bp; + const dbp: IDataBreakpoint = bp; return { type: 'data', id: dbp.getId(), @@ -458,9 +466,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb label: dbp.description, canPersist: dbp.canPersist } satisfies IDataBreakpointDto; - } else { - const sbp = bp; - return { + } else if ('uri' in bp) { + const sbp: IBreakpoint = bp; + return { type: 'source', id: sbp.getId(), enabled: sbp.enabled, @@ -470,9 +478,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb uri: sbp.uri, line: sbp.lineNumber > 0 ? sbp.lineNumber - 1 : 0, character: (typeof sbp.column === 'number' && sbp.column > 0) ? sbp.column - 1 : 0, - }; + } satisfies ISourceBreakpointDto; + } else { + return undefined; } - }); + }).filter(isDefined); } } diff --git a/src/vs/workbench/api/browser/mainThreadDecorations.ts b/src/vs/workbench/api/browser/mainThreadDecorations.ts index 76ec7391e01..46ae4275baa 100644 --- a/src/vs/workbench/api/browser/mainThreadDecorations.ts +++ b/src/vs/workbench/api/browser/mainThreadDecorations.ts @@ -87,13 +87,13 @@ export class MainThreadDecorations implements MainThreadDecorationsShape { const registration = this._decorationsService.registerDecorationsProvider({ label, onDidChange: emitter.event, - provideDecorations: async (uri, token) => { + provideDecorations: async (uri, token): Promise => { const data = await queue.enqueue(uri, token); if (!data) { return undefined; } const [bubble, tooltip, letter, themeColor] = data; - return { + return { weight: 10, bubble: bubble ?? false, color: themeColor?.id, diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index e0f966a9fb2..9bf27279db0 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -3,30 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, AnyInputDto, TabInputKind, TabModelOperationKind, TextDiffInputDto } from 'vs/workbench/api/common/extHost.protocol'; -import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { Event } from 'vs/base/common/event'; +import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AnyInputDto, ExtHostContext, IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextDiffInputDto } from 'vs/workbench/api/common/extHost.protocol'; import { EditorResourceAccessor, GroupModelChangeKind, SideBySideEditor } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { isGroupEditorMoveEvent } from 'vs/workbench/common/editor/editorGroupModel'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; +import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { columnToEditorGroup, EditorGroupColumn, editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { GroupDirection, IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorsChangeEvent, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; -import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; -import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; -import { URI } from 'vs/base/common/uri'; -import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; -import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; -import { isEqual } from 'vs/base/common/resources'; -import { isGroupEditorMoveEvent } from 'vs/workbench/common/editor/editorGroupModel'; -import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput'; -import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; -import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; interface TabInfo { tab: IEditorTabDto; @@ -44,6 +45,8 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { private readonly _groupLookup: Map = new Map(); // Lookup table for finding tab by id private readonly _tabInfoLookup: Map = new Map(); + // Tracks the currently open MultiDiffEditorInputs to listen to resource changes + private readonly _multiDiffEditorInputListeners: DisposableMap = new DisposableMap(); constructor( extHostContext: IExtHostContext, @@ -65,6 +68,8 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } })); + this._dispoables.add(this._multiDiffEditorInputListeners); + // Structural group changes (add, remove, move, etc) are difficult to patch. // Since they happen infrequently we just rebuild the entire model this._dispoables.add(this._editorGroupsService.onDidAddGroup(() => this._createTabsModel())); @@ -196,18 +201,17 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { if (editor instanceof ChatEditorInput) { return { kind: TabInputKind.ChatEditorInput, - providerId: editor.providerId ?? 'unknown', }; } if (editor instanceof MultiDiffEditorInput) { const diffEditors: TextDiffInputDto[] = []; - for (const resource of (editor?.initialResources ?? [])) { - if (resource.original && resource.modified) { + for (const resource of (editor?.resources.get() ?? [])) { + if (resource.originalUri && resource.modifiedUri) { diffEditors.push({ kind: TabInputKind.TextDiffInput, - original: resource.original, - modified: resource.modified + original: resource.originalUri, + modified: resource.modifiedUri }); } } @@ -298,7 +302,24 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { const tabObject = this._buildTabObject(group, editorInput, editorIndex); tabs.splice(editorIndex, 0, tabObject); // Update lookup - this._tabInfoLookup.set(this._generateTabId(editorInput, groupId), { group, editorInput, tab: tabObject }); + const tabId = this._generateTabId(editorInput, groupId); + this._tabInfoLookup.set(tabId, { group, editorInput, tab: tabObject }); + + if (editorInput instanceof MultiDiffEditorInput) { + this._multiDiffEditorInputListeners.set(editorInput, Event.fromObservableLight(editorInput.resources)(() => { + const tabInfo = this._tabInfoLookup.get(tabId); + if (!tabInfo) { + return; + } + tabInfo.tab = this._buildTabObject(group, editorInput, editorIndex); + this._proxy.$acceptTabOperation({ + groupId, + index: editorIndex, + tabDto: tabInfo.tab, + kind: TabModelOperationKind.TAB_UPDATE + }); + })); + } this._proxy.$acceptTabOperation({ groupId, @@ -332,6 +353,10 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { // Update lookup this._tabInfoLookup.delete(removedTab[0]?.id ?? ''); + if (removedTab[0]?.input instanceof MultiDiffEditorInput) { + this._multiDiffEditorInputListeners.deleteAndDispose(removedTab[0]?.input); + } + this._proxy.$acceptTabOperation({ groupId, index: editorIndex, @@ -472,6 +497,10 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { * Builds the model from scratch based on the current state of the editor service. */ private _createTabsModel(): void { + if (this._editorGroupsService.groups.length === 0) { + return; // skip this invalid state, it may happen when the entire editor area is transitioning to other state ("editor working sets") + } + this._tabGroupModel = []; this._groupLookup.clear(); this._tabInfoLookup.clear(); diff --git a/src/vs/workbench/api/browser/mainThreadEmbeddings.ts b/src/vs/workbench/api/browser/mainThreadEmbeddings.ts new file mode 100644 index 00000000000..776c471e15f --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadEmbeddings.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ExtHostContext, ExtHostEmbeddingsShape, MainContext, MainThreadEmbeddingsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; + + +interface IEmbeddingsProvider { + provideEmbeddings(input: string[], token: CancellationToken): Promise<{ values: number[] }[]>; +} + +const IEmbeddingsService = createDecorator('embeddingsService'); + +interface IEmbeddingsService { + + _serviceBrand: undefined; + + readonly onDidChange: Event; + + allProviders: Iterable; + + registerProvider(id: string, provider: IEmbeddingsProvider): IDisposable; + + computeEmbeddings(id: string, input: string[], token: CancellationToken): Promise<{ values: number[] }[]>; +} + +class EmbeddingsService implements IEmbeddingsService { + _serviceBrand: undefined; + + private providers: Map; + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + constructor() { + this.providers = new Map(); + } + + get allProviders(): Iterable { + return this.providers.keys(); + } + + registerProvider(id: string, provider: IEmbeddingsProvider): IDisposable { + this.providers.set(id, provider); + this._onDidChange.fire(); + return { + dispose: () => { + this.providers.delete(id); + this._onDidChange.fire(); + } + }; + } + + computeEmbeddings(id: string, input: string[], token: CancellationToken): Promise<{ values: number[] }[]> { + const provider = this.providers.get(id); + if (provider) { + return provider.provideEmbeddings(input, token); + } else { + return Promise.reject(new Error(`No embeddings provider registered with id: ${id}`)); + } + } +} + + +registerSingleton(IEmbeddingsService, EmbeddingsService, InstantiationType.Delayed); + +@extHostNamedCustomer(MainContext.MainThreadEmbeddings) +export class MainThreadEmbeddings implements MainThreadEmbeddingsShape { + + private readonly _store = new DisposableStore(); + private readonly _providers = this._store.add(new DisposableMap); + private readonly _proxy: ExtHostEmbeddingsShape; + + constructor( + context: IExtHostContext, + @IEmbeddingsService private readonly embeddingsService: IEmbeddingsService + ) { + this._proxy = context.getProxy(ExtHostContext.ExtHostEmbeddings); + + this._store.add(embeddingsService.onDidChange((() => { + this._proxy.$acceptEmbeddingModels(Array.from(embeddingsService.allProviders)); + }))); + } + + dispose(): void { + this._store.dispose(); + } + + $registerEmbeddingProvider(handle: number, identifier: string): void { + const registration = this.embeddingsService.registerProvider(identifier, { + provideEmbeddings: (input: string[], token: CancellationToken): Promise<{ values: number[] }[]> => { + return this._proxy.$provideEmbeddings(handle, input, token); + } + }); + this._providers.set(handle, registration); + } + + $unregisterEmbeddingProvider(handle: number): void { + this._providers.deleteAndDispose(handle); + } + + $computeEmbeddings(embeddingsModel: string, input: string[], token: CancellationToken): Promise<{ values: number[] }[]> { + return this.embeddingsService.computeEmbeddings(embeddingsModel, input, token); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadErrors.ts b/src/vs/workbench/api/browser/mainThreadErrors.ts index 2d05a6a0e44..e1591179944 100644 --- a/src/vs/workbench/api/browser/mainThreadErrors.ts +++ b/src/vs/workbench/api/browser/mainThreadErrors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SerializedError, onUnexpectedError, ErrorNoTelemetry } from 'vs/base/common/errors'; +import { SerializedError, onUnexpectedError, transformErrorFromSerialization } from 'vs/base/common/errors'; import { extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { MainContext, MainThreadErrorsShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -16,11 +16,7 @@ export class MainThreadErrors implements MainThreadErrorsShape { $onUnexpectedError(err: any | SerializedError): void { if (err && err.$isError) { - const { name, message, stack } = err; - err = err.noTelemetry ? new ErrorNoTelemetry() : new Error(); - err.message = message; - err.name = name; - err.stack = stack; + err = transformErrorFromSerialization(err); } onUnexpectedError(err); } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 6732d38f5e6..7c4db0a5def 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -6,7 +6,7 @@ import { Action } from 'vs/base/common/actions'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { SerializedError } from 'vs/base/common/errors'; +import { SerializedError, transformErrorFromSerialization } from 'vs/base/common/errors'; import { FileAccess } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -73,19 +73,13 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha this._internalExtensionService._onDidActivateExtension(extensionId, codeLoadingTime, activateCallTime, activateResolvedTime, activationReason); } $onExtensionRuntimeError(extensionId: ExtensionIdentifier, data: SerializedError): void { - const error = new Error(); - error.name = data.name; - error.message = data.message; - error.stack = data.stack; + const error = transformErrorFromSerialization(data); this._internalExtensionService._onExtensionRuntimeError(extensionId, error); console.error(`[${extensionId.value}]${error.message}`); console.error(error.stack); } async $onExtensionActivationError(extensionId: ExtensionIdentifier, data: SerializedError, missingExtensionDependency: MissingExtensionDependency | null): Promise { - const error = new Error(); - error.name = data.name; - error.message = data.message; - error.stack = data.stack; + const error = transformErrorFromSerialization(data); this._internalExtensionService._onDidActivateExtensionError(extensionId, error); diff --git a/src/vs/workbench/api/browser/mainThreadInlineChat.ts b/src/vs/workbench/api/browser/mainThreadInlineChat.ts deleted file mode 100644 index 48207f86ffa..00000000000 --- a/src/vs/workbench/api/browser/mainThreadInlineChat.ts +++ /dev/null @@ -1,82 +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 { DisposableMap } from 'vs/base/common/lifecycle'; -import { IInlineChatBulkEditResponse, IInlineChatProgressItem, IInlineChatResponse, IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { reviveWorkspaceEditDto } from 'vs/workbench/api/browser/mainThreadBulkEdits'; -import { ExtHostContext, ExtHostInlineChatShape, MainContext, MainThreadInlineChatShape as MainThreadInlineChatShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IProgress } from 'vs/platform/progress/common/progress'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; - -@extHostNamedCustomer(MainContext.MainThreadInlineChat) -export class MainThreadInlineChat implements MainThreadInlineChatShape { - - private readonly _registrations = new DisposableMap(); - private readonly _proxy: ExtHostInlineChatShape; - - private readonly _progresses = new Map>(); - - constructor( - extHostContext: IExtHostContext, - @IInlineChatService private readonly _inlineChatService: IInlineChatService, - @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, - ) { - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostInlineChat); - } - - dispose(): void { - this._registrations.dispose(); - } - - async $registerInteractiveEditorProvider(handle: number, label: string, extensionId: ExtensionIdentifier, supportsFeedback: boolean, supportsFollowups: boolean, supportIssueReporting: boolean): Promise { - const unreg = this._inlineChatService.addProvider({ - extensionId, - label, - supportIssueReporting, - prepareInlineChatSession: async (model, range, token) => { - const session = await this._proxy.$prepareSession(handle, model.uri, range, token); - if (!session) { - return undefined; - } - return { - ...session, - dispose: () => { - this._proxy.$releaseSession(handle, session.id); - } - }; - }, - provideResponse: async (item, request, progress, token) => { - this._progresses.set(request.requestId, progress); - try { - const result = await this._proxy.$provideResponse(handle, item, request, token); - if (result?.type === 'bulkEdit') { - (result).edits = reviveWorkspaceEditDto(result.edits, this._uriIdentService); - } - return result; - } finally { - this._progresses.delete(request.requestId); - } - }, - provideFollowups: !supportsFollowups ? undefined : async (session, response, token) => { - return this._proxy.$provideFollowups(handle, session.id, response.id, token); - }, - handleInlineChatResponseFeedback: !supportsFeedback ? undefined : async (session, response, kind) => { - this._proxy.$handleFeedback(handle, session.id, response.id, kind); - } - }); - - this._registrations.set(handle, unreg); - } - - async $handleProgressChunk(requestId: string, chunk: IInlineChatProgressItem): Promise { - await Promise.resolve(this._progresses.get(requestId)?.report(chunk)); - } - - async $unregisterInteractiveEditorProvider(handle: number): Promise { - this._registrations.deleteAndDispose(handle); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadIssueReporter.ts b/src/vs/workbench/api/browser/mainThreadIssueReporter.ts deleted file mode 100644 index c3afade0cbb..00000000000 --- a/src/vs/workbench/api/browser/mainThreadIssueReporter.ts +++ /dev/null @@ -1,58 +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 'vs/base/common/cancellation'; -import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { ExtHostContext, ExtHostIssueReporterShape, MainContext, MainThreadIssueReporterShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IIssueDataProvider, IIssueUriRequestHandler, IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; - -@extHostNamedCustomer(MainContext.MainThreadIssueReporter) -export class MainThreadIssueReporter extends Disposable implements MainThreadIssueReporterShape { - private readonly _proxy: ExtHostIssueReporterShape; - private readonly _registrationsUri = this._register(new DisposableMap()); - private readonly _registrationsData = this._register(new DisposableMap()); - - constructor( - context: IExtHostContext, - @IWorkbenchIssueService private readonly _issueService: IWorkbenchIssueService - ) { - super(); - this._proxy = context.getProxy(ExtHostContext.ExtHostIssueReporter); - } - - $registerIssueUriRequestHandler(extensionId: string): void { - const handler: IIssueUriRequestHandler = { - provideIssueUrl: async (token: CancellationToken) => { - const parts = await this._proxy.$getIssueReporterUri(extensionId, token); - return URI.from(parts); - } - }; - this._registrationsUri.set(extensionId, this._issueService.registerIssueUriRequestHandler(extensionId, handler)); - } - - $unregisterIssueUriRequestHandler(extensionId: string): void { - this._registrationsUri.deleteAndDispose(extensionId); - } - - $registerIssueDataProvider(extensionId: string): void { - const provider: IIssueDataProvider = { - provideIssueExtensionData: async (token: CancellationToken) => { - const parts = await this._proxy.$getIssueReporterData(extensionId, token); - return parts; - }, - provideIssueExtensionTemplate: async (token: CancellationToken) => { - const parts = await this._proxy.$getIssueReporterTemplate(extensionId, token); - return parts; - } - }; - this._registrationsData.set(extensionId, this._issueService.registerIssueDataProvider(extensionId, provider)); - } - - $unregisterIssueDataProvider(extensionId: string): void { - this._registrationsData.deleteAndDispose(extensionId); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index d89eecf9a3f..629af25ae4c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -8,13 +8,15 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { createStringDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { combinedDisposable, Disposable, DisposableMap, toDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { revive } from 'vs/base/common/marshalling'; import { mixin } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; -import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; -import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; +import { Position as EditorPosition, IPosition } from 'vs/editor/common/core/position'; +import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as languages from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -32,10 +34,7 @@ import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy' import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; -import { ResourceMap } from 'vs/base/common/map'; -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -67,7 +66,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } this._proxy.$setWordDefinitions(wordDefinitionDtos); }; - this._languageConfigurationService.onDidChange((e) => { + this._register(this._languageConfigurationService.onDidChange((e) => { if (!e.languageId) { updateAllWordDefinitions(); } else { @@ -78,7 +77,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread regexFlags: wordDefinition.flags }]); } - }); + })); updateAllWordDefinitions(); } } @@ -163,7 +162,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- outline $registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void { - this._registrations.set(handle, this._languageFeaturesService.documentSymbolProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.documentSymbolProvider.register(selector, { displayName, provideDocumentSymbols: (model: ITextModel, token: CancellationToken): Promise => { return this._proxy.$provideDocumentSymbols(handle, model.uri, token); @@ -175,7 +174,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread $registerCodeLensSupport(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { - const provider = { + const provider: languages.CodeLensProvider = { provideCodeLenses: async (model: ITextModel, token: CancellationToken): Promise => { const listDto = await this._proxy.$provideCodeLenses(handle, model.uri, token); if (!listDto) { @@ -217,7 +216,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- declaration $registerDefinitionSupport(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.definitionProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.definitionProvider.register(selector, { provideDefinition: (model, position, token): Promise => { return this._proxy.$provideDefinition(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto); } @@ -225,7 +224,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } $registerDeclarationSupport(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.declarationProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.declarationProvider.register(selector, { provideDeclaration: (model, position, token) => { return this._proxy.$provideDeclaration(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto); } @@ -233,7 +232,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } $registerImplementationSupport(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.implementationProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.implementationProvider.register(selector, { provideImplementation: (model, position, token): Promise => { return this._proxy.$provideImplementation(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto); } @@ -241,7 +240,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } $registerTypeDefinitionSupport(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.typeDefinitionProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.typeDefinitionProvider.register(selector, { provideTypeDefinition: (model, position, token): Promise => { return this._proxy.$provideTypeDefinition(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto); } @@ -251,9 +250,22 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- extra info $registerHoverProvider(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.hoverProvider.register(selector, { - provideHover: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { - return this._proxy.$provideHover(handle, model.uri, position, token); + /* + const hoverFinalizationRegistry = new FinalizationRegistry((hoverId: number) => { + this._proxy.$releaseHover(handle, hoverId); + }); + */ + this._registrations.set(handle, this._languageFeaturesService.hoverProvider.register(selector, { + provideHover: async (model: ITextModel, position: EditorPosition, token: CancellationToken, context?: languages.HoverContext): Promise => { + const serializedContext: languages.HoverContext<{ id: number }> = { + verbosityRequest: context?.verbosityRequest ? { + verbosityDelta: context.verbosityRequest.verbosityDelta, + previousHover: { id: context.verbosityRequest.previousHover.id } + } : undefined, + }; + const hover = await this._proxy.$provideHover(handle, model.uri, position, serializedContext, token); + // hoverFinalizationRegistry.register(hover, hover.id); + return hover; } })); } @@ -261,7 +273,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- debug hover $registerEvaluatableExpressionProvider(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.evaluatableExpressionProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.evaluatableExpressionProvider.register(selector, { provideEvaluatableExpression: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { return this._proxy.$provideEvaluatableExpression(handle, model.uri, position, token); } @@ -271,7 +283,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- inline values $registerInlineValuesProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { - const provider = { + const provider: languages.InlineValuesProvider = { provideInlineValues: (model: ITextModel, viewPort: EditorRange, context: languages.InlineValueContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineValues(handle, model.uri, viewPort, context, token); } @@ -296,7 +308,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- occurrences $registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.documentHighlightProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.documentHighlightProvider.register(selector, { provideDocumentHighlights: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { return this._proxy.$provideDocumentHighlights(handle, model.uri, position, token); } @@ -304,11 +316,13 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } $registerMultiDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.multiDocumentHighlightProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.multiDocumentHighlightProvider.register(selector, { selector: selector, provideMultiDocumentHighlights: (model: ITextModel, position: EditorPosition, otherModels: ITextModel[], token: CancellationToken): Promise | undefined> => { return this._proxy.$provideMultiDocumentHighlights(handle, model.uri, position, otherModels.map(model => model.uri), token).then(dto => { - if (isFalsyOrEmpty(dto)) { + // dto should be non-null + non-undefined + // dto length of 0 is valid, just no highlights, pass this through. + if (dto === undefined || dto === null) { return undefined; } const result = new ResourceMap(); @@ -330,7 +344,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- linked editing $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.linkedEditingRangeProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.linkedEditingRangeProvider.register(selector, { provideLinkedEditingRanges: async (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { const res = await this._proxy.$provideLinkedEditingRanges(handle, model.uri, position, token); if (res) { @@ -347,23 +361,23 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- references $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void { - this._registrations.set(handle, this._languageFeaturesService.referenceProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.referenceProvider.register(selector, { provideReferences: (model: ITextModel, position: EditorPosition, context: languages.ReferenceContext, token: CancellationToken): Promise => { return this._proxy.$provideReferences(handle, model.uri, position, context, token).then(MainThreadLanguageFeatures._reviveLocationDto); } })); } - // --- quick fix + // --- code actions - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void { + $registerCodeActionSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, extensionId: string, supportsResolve: boolean): void { const provider: languages.CodeActionProvider = { provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: languages.CodeActionContext, token: CancellationToken): Promise => { const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token); if (!listDto) { return undefined; } - return { + return { actions: MainThreadLanguageFeatures._reviveCodeActionDto(listDto.actions, this._uriIdentService), dispose: () => { if (typeof listDto.cacheId === 'number') { @@ -374,7 +388,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }, providedCodeActionKinds: metadata.providedKinds, documentation: metadata.documentation, - displayName + displayName, + extensionId, }; if (supportsResolve) { @@ -492,8 +507,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread $registerNewSymbolNamesProvider(handle: number, selector: IDocumentFilterDto[]): void { this._registrations.set(handle, this._languageFeaturesService.newSymbolNamesProvider.register(selector, { - provideNewSymbolNames: (model: ITextModel, range: IRange, token: CancellationToken): Promise => { - return this._proxy.$provideNewSymbolNames(handle, model.uri, range, token); + supportsAutomaticNewSymbolNamesTriggerKind: this._proxy.$supportsAutomaticNewSymbolNamesTriggerKind(handle), + provideNewSymbolNames: (model: ITextModel, range: IRange, triggerKind: languages.NewSymbolNameTriggerKind, token: CancellationToken): Promise => { + return this._proxy.$provideNewSymbolNames(handle, model.uri, range, triggerKind, token); } } satisfies languages.NewSymbolNamesProvider)); } @@ -597,6 +613,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); }, + provideInlineEdits: async (model: ITextModel, range: EditorRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { + return this._proxy.$provideInlineEdits(handle, model.uri, range, context, token); + }, handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion, updatedInsertText: string): Promise => { if (supportsHandleEvents) { await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); @@ -635,7 +654,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- parameter hints $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void { - this._registrations.set(handle, this._languageFeaturesService.signatureHelpProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.signatureHelpProvider.register(selector, { signatureHelpTriggerCharacters: metadata.triggerCharacters, signatureHelpRetriggerCharacters: metadata.retriggerCharacters, @@ -658,7 +677,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- inline hints $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void { - const provider = { + const provider: languages.InlayHintsProvider = { displayName, provideInlayHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { const result = await this._proxy.$provideInlayHints(handle, model.uri, range, token); @@ -750,7 +769,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void { const proxy = this._proxy; - this._registrations.set(handle, this._languageFeaturesService.colorProvider.register(selector, { + this._registrations.set(handle, this._languageFeaturesService.colorProvider.register(selector, { provideDocumentColors: (model, token) => { return proxy.$provideDocumentColors(handle, model.uri, token) .then(documentColors => { @@ -783,7 +802,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- folding $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, eventHandle: number | undefined): void { - const provider = { + const provider: languages.FoldingRangeProvider = { id: extensionId.value, provideFoldingRanges: (model, context, token) => { return this._proxy.$provideFoldingRanges(handle, model.uri, context, token); @@ -968,7 +987,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread const provider = new MainThreadDocumentOnDropEditProvider(handle, this._proxy, metadata, this._uriIdentService); this._documentOnDropEditProviders.set(handle, provider); this._registrations.set(handle, combinedDisposable( - this._languageFeaturesService.documentOnDropEditProvider.register(selector, provider), + this._languageFeaturesService.documentDropEditProvider.register(selector, provider), toDisposable(() => this._documentOnDropEditProviders.delete(handle)), )); } @@ -983,8 +1002,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- mapped edits - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void { - const provider = new MainThreadMappedEditsProvider(handle, this._proxy, this._uriIdentService); + $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void { + const provider = new MainThreadMappedEditsProvider(displayName, handle, this._proxy, this._uriIdentService); this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider)); } } @@ -1082,12 +1101,14 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider } } -class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEditProvider { +class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEditProvider { private readonly dataTransfers = new DataTransferFileCache(); readonly dropMimeTypes?: readonly string[]; + readonly resolveDocumentDropEdit?: languages.DocumentDropEditProvider['resolveDocumentDropEdit']; + constructor( private readonly _handle: number, private readonly _proxy: ExtHostLanguageFeaturesShape, @@ -1095,9 +1116,19 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd @IUriIdentityService private readonly _uriIdentService: IUriIdentityService ) { this.dropMimeTypes = metadata?.dropMimeTypes ?? ['*/*']; + + if (metadata?.supportsResolve) { + this.resolveDocumentDropEdit = async (edit, token) => { + const resolved = await this._proxy.$resolvePasteEdit(this._handle, (edit)._cacheId!, token); + if (resolved.additionalEdit) { + edit.additionalEdit = reviveWorkspaceEditDto(resolved.additionalEdit, this._uriIdentService); + } + return edit; + }; + } } - async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const request = this.dataTransfers.add(dataTransfer); try { const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer); @@ -1110,14 +1141,19 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd return; } - return edits.map(edit => { - return { - ...edit, - yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), - kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, - additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), - }; - }); + return { + edits: edits.map(edit => { + return { + ...edit, + yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), + kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, + additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), + }; + }), + dispose: () => { + this._proxy.$releaseDocumentOnDropEdits(this._handle, request.id); + }, + }; } finally { request.dispose(); } @@ -1206,6 +1242,7 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages. export class MainThreadMappedEditsProvider implements languages.MappedEditsProvider { constructor( + public readonly displayName: string, private readonly _handle: number, private readonly _proxy: ExtHostLanguageFeaturesShape, private readonly _uriService: IUriIdentityService, diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts new file mode 100644 index 00000000000..1e1a25f966c --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/cancellation'; +import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; +import { ExtHostLanguageModelToolsShape, ExtHostContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IToolData, ILanguageModelToolsService, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) +export class MainThreadLanguageModelTools extends Disposable implements MainThreadLanguageModelToolsShape { + + private readonly _proxy: ExtHostLanguageModelToolsShape; + private readonly _tools = this._register(new DisposableMap()); + + constructor( + extHostContext: IExtHostContext, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageModelTools); + + this._register(this._languageModelToolsService.onDidChangeTools(e => this._proxy.$acceptToolDelta(e))); + } + + async $getTools(): Promise { + return Array.from(this._languageModelToolsService.getTools()); + } + + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + return this._languageModelToolsService.invokeTool(name, parameters, token); + } + + $registerTool(name: string): void { + const disposable = this._languageModelToolsService.registerToolImplementation( + name, + { + invoke: async (parameters, token) => { + return await this._proxy.$invokeTool(name, parameters, token); + }, + }); + this._tools.set(name, disposable); + } + + $unregisterTool(name: string): void { + this._tools.deleteAndDispose(name); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 9a886a6713a..adea665487c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -3,20 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from 'vs/base/common/arrays'; +import { AsyncIterableSource, DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress, Progress } from 'vs/platform/progress/common/progress'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; +import { ILanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; +import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector, ILanguageModelChatResponse } from 'vs/workbench/contrib/chat/common/languageModels'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; -import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -26,21 +25,20 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { private readonly _proxy: ExtHostLanguageModelsShape; private readonly _store = new DisposableStore(); private readonly _providerRegistrations = new DisposableMap(); - private readonly _pendingProgress = new Map>(); + private readonly _pendingProgress = new Map; stream: AsyncIterableSource }>(); constructor( extHostContext: IExtHostContext, @ILanguageModelsService private readonly _chatProviderService: ILanguageModelsService, - @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, + @ILanguageModelStatsService private readonly _languageModelStatsService: ILanguageModelStatsService, @ILogService private readonly _logService: ILogService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService, @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); - - this._proxy.$updateLanguageModels({ added: coalesce(_chatProviderService.getLanguageModelIds().map(id => _chatProviderService.lookupLanguageModel(id))) }); - this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$updateLanguageModels, this._proxy)); + this._proxy.$acceptChatModelMetadata({ added: _chatProviderService.getLanguageModelIds().map(id => ({ identifier: id, metadata: _chatProviderService.lookupLanguageModel(id)! })) }); + this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$acceptChatModelMetadata, this._proxy)); } dispose(): void { @@ -52,71 +50,105 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { const dipsosables = new DisposableStore(); dipsosables.add(this._chatProviderService.registerLanguageModelChat(identifier, { metadata, - provideChatResponse: async (messages, from, options, progress, token) => { + sendChatRequest: async (messages, from, options, token) => { const requestId = (Math.random() * 1e6) | 0; - this._pendingProgress.set(requestId, progress); + const defer = new DeferredPromise(); + const stream = new AsyncIterableSource(); + try { - await this._proxy.$provideLanguageModelResponse(handle, requestId, from, messages, options, token); - } finally { + this._pendingProgress.set(requestId, { defer, stream }); + await this._proxy.$startChatRequest(handle, requestId, from, messages, options, token); + } catch (err) { this._pendingProgress.delete(requestId); + throw err; } - } + + return { + result: defer.p, + stream: stream.asyncIterable + } satisfies ILanguageModelChatResponse; + }, + provideTokenCount: (str, token) => { + return this._proxy.$provideTokenLength(handle, str, token); + }, })); if (metadata.auth) { dipsosables.add(this._registerAuthenticationProvider(metadata.extension, metadata.auth)); } - dipsosables.add(Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ - id: `lm-${identifier}`, - label: localize('languageModels', "Language Model ({0})", `${identifier}`), - access: { - canToggle: false, - }, - })); this._providerRegistrations.set(handle, dipsosables); } - async $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise { - this._pendingProgress.get(requestId)?.report(chunk); + async $reportResponsePart(requestId: number, chunk: IChatResponseFragment): Promise { + const data = this._pendingProgress.get(requestId); + this._logService.trace('[LM] report response PART', Boolean(data), requestId, chunk); + if (data) { + data.stream.emitOne(chunk); + } + } + + async $reportResponseDone(requestId: number, err: SerializedError | undefined): Promise { + const data = this._pendingProgress.get(requestId); + this._logService.trace('[LM] report response DONE', Boolean(data), requestId, err); + if (data) { + this._pendingProgress.delete(requestId); + if (err) { + const error = transformErrorFromSerialization(err); + data.stream.reject(error); + data.defer.error(error); + } else { + data.stream.resolve(); + data.defer.complete(undefined); + } + } } $unregisterProvider(handle: number): void { this._providerRegistrations.deleteAndDispose(handle); } - async $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise { - - const activate = this._extensionService.activateByEvent(`onLanguageModelAccess:${providerId}`); - const metadata = this._chatProviderService.lookupLanguageModel(providerId); - - if (metadata) { - return metadata; - } - - await Promise.race([ - activate, - Event.toPromise(Event.filter(this._chatProviderService.onDidChangeLanguageModels, e => Boolean(e.added?.some(value => value.identifier === providerId)))) - ]); - - return this._chatProviderService.lookupLanguageModel(providerId); + $selectChatModels(selector: ILanguageModelChatSelector): Promise { + return this._chatProviderService.selectLanguageModels(selector); } - async $fetchResponse(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { - await this._extensionFeaturesManagementService.getAccess(extension, `lm-${providerId}`); + $whenLanguageModelChatRequestMade(identifier: string, extensionId: ExtensionIdentifier, participant?: string | undefined, tokenCount?: number | undefined): void { + this._languageModelStatsService.update(identifier, extensionId, participant, tokenCount); + } - this._logService.debug('[CHAT] extension request STARTED', extension.value, requestId); + async $tryStartChatRequest(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { + this._logService.trace('[CHAT] request STARTED', extension.value, requestId); - const task = this._chatProviderService.makeLanguageModelChatRequest(providerId, extension, messages, options, new Progress(value => { - this._proxy.$handleResponseFragment(requestId, value); - }), token); + const response = await this._chatProviderService.sendChatRequest(providerId, extension, messages, options, token); - task.catch(err => { - this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); - throw err; - }).finally(() => { + // !!! IMPORTANT !!! + // This method must return before the response is done (has streamed all parts) + // and because of that we consume the stream without awaiting + // !!! IMPORTANT !!! + const streaming = (async () => { + try { + for await (const part of response.stream) { + this._logService.trace('[CHAT] request PART', extension.value, requestId, part); + await this._proxy.$acceptResponsePart(requestId, part); + } + this._logService.trace('[CHAT] request DONE', extension.value, requestId); + } catch (err) { + this._logService.error('[CHAT] extension request ERRORED in STREAM', err, extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); + } + })(); + + // When the response is done (signaled via its result) we tell the EH + Promise.allSettled([response.result, streaming]).then(() => { this._logService.debug('[CHAT] extension request DONE', extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, undefined); + }, err => { + this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); }); + } - return task; + + $countTokens(provider: string, value: string | IChatMessage, token: CancellationToken): Promise { + return this._chatProviderService.computeTokenLength(provider, value, token); } private _registerAuthenticationProvider(extension: ExtensionIdentifier, auth: { providerLabel: string; accountLabel?: string | undefined }): IDisposable { @@ -174,9 +206,9 @@ class LanguageModelAccessAuthProvider implements IAuthenticationProvider { if (this._session) { return [this._session]; } - return [await this.createSession(scopes || [], {})]; + return [await this.createSession(scopes || [])]; } - async createSession(scopes: string[], options: IAuthenticationProviderCreateSessionOptions): Promise { + async createSession(scopes: string[]): Promise { this._session = this._createFakeSession(scopes); this._onDidChangeSessions.fire({ added: [this._session], changed: [], removed: [] }); return this._session; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index c036901f907..b50fee3527c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -104,7 +104,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { }; } - const thisPriorityInfo = coalesce([{ isFromSettings: false, filenamePatterns: includes }, ...allPriorityInfo.get(viewType) ?? []]); + const thisPriorityInfo = coalesce([{ isFromSettings: false, filenamePatterns: includes }, ...allPriorityInfo.get(viewType) ?? []]); const otherEditorsPriorityInfo = Array.from(allPriorityInfo.keys()) .flatMap(key => { if (key !== viewType) { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 8f2f658e810..5801f7845d9 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Barrier } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, DisposableStore, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; +import { derived, observableValue, observableValueOpts } from 'vs/base/common/observable'; +import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -20,6 +22,11 @@ import { ResourceTree } from 'vs/base/common/resourceTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { basename } from 'vs/base/common/resources'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { Schemas } from 'vs/base/common/network'; +import { ITextModel } from 'vs/editor/common/model'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -34,6 +41,32 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } } +function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { + const icon = getIconFromIconDto(historyItemDto.icon); + const labels = historyItemDto.labels?.map(l => ({ title: l.title, icon: getIconFromIconDto(l.icon) })); + + return { ...historyItemDto, icon, labels }; +} + +class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { + constructor( + textModelService: ITextModelService, + private readonly modelService: IModelService, + private readonly languageService: ILanguageService, + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeSourceControl, this)); + } + + async provideTextContent(resource: URI): Promise { + const existing = this.modelService.getModel(resource); + if (existing) { + return existing; + } + return this.modelService.createModel('', this.languageService.createById('scminput'), resource); + } +} + class MainThreadSCMResourceGroup implements ISCMResourceGroup { readonly resources: ISCMResource[] = []; @@ -127,16 +160,11 @@ class MainThreadSCMResource implements ISCMResource { } class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { + readonly currentHistoryItemGroupId = derived(this, reader => this.currentHistoryItemGroup.read(reader)?.id); + readonly currentHistoryItemGroupName = derived(this, reader => this.currentHistoryItemGroup.read(reader)?.name); - private _onDidChangeCurrentHistoryItemGroup = new Emitter(); - readonly onDidChangeCurrentHistoryItemGroup = this._onDidChangeCurrentHistoryItemGroup.event; - - private _currentHistoryItemGroup: ISCMHistoryItemGroup | undefined; - get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } - set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined) { - this._currentHistoryItemGroup = historyItemGroup; - this._onDidChangeCurrentHistoryItemGroup.fire(); - } + private readonly _currentHistoryItemGroup = observableValueOpts({ owner: this, equalsFn: () => false }, undefined); + get currentHistoryItemGroup() { return this._currentHistoryItemGroup; } constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } @@ -144,14 +172,23 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupId1, historyItemGroupId2, CancellationToken.None); } + async resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds: string[]): Promise { + return this.proxy.$resolveHistoryItemGroupCommonAncestor2(this.handle, historyItemGroupIds, CancellationToken.None); + } + async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None); - return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) })); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); + } + + async provideHistoryItems2(options: ISCMHistoryOptions): Promise { + const historyItems = await this.proxy.$provideHistoryItems2(this.handle, options, CancellationToken.None); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); } async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { const historyItem = await this.proxy.$provideHistoryItemSummary(this.handle, historyItemId, historyItemParentId, CancellationToken.None); - return historyItem ? { ...historyItem, icon: getIconFromIconDto(historyItem.icon) } : undefined; + return historyItem ? toISCMHistoryItem(historyItem) : undefined; } async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise { @@ -164,6 +201,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { })); } + $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { + this._currentHistoryItemGroup.set(historyItemGroup, undefined); + } } class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { @@ -197,27 +237,23 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get handle(): number { return this._handle; } get label(): string { return this._label; } get rootUri(): URI | undefined { return this._rootUri; } - get inputBoxDocumentUri(): URI { return this._inputBoxDocumentUri; } + get inputBoxTextModel(): ITextModel { return this._inputBoxTextModel; } get contextValue(): string { return this._providerId; } - get commitTemplate(): string { return this.features.commitTemplate || ''; } - get historyProvider(): ISCMHistoryProvider | undefined { return this._historyProvider; } get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; } get actionButton(): ISCMActionButtonDescriptor | undefined { return this.features.actionButton ?? undefined; } - get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; } - get count(): number | undefined { return this.features.count; } + + private readonly _count = observableValue(this, undefined); + get count() { return this._count; } + + private readonly _statusBarCommands = observableValue(this, undefined); + get statusBarCommands() { return this._statusBarCommands; } private readonly _name: string | undefined; get name(): string { return this._name ?? this._label; } - private readonly _onDidChangeCommitTemplate = new Emitter(); - readonly onDidChangeCommitTemplate: Event = this._onDidChangeCommitTemplate.event; - - private readonly _onDidChangeStatusBarCommands = new Emitter(); - get onDidChangeStatusBarCommands(): Event { return this._onDidChangeStatusBarCommands.event; } - - private readonly _onDidChangeHistoryProvider = new Emitter(); - readonly onDidChangeHistoryProvider: Event = this._onDidChangeHistoryProvider.event; + private readonly _commitTemplate = observableValue(this, ''); + get commitTemplate() { return this._commitTemplate; } private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; @@ -225,7 +261,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private _quickDiff: IDisposable | undefined; public readonly isSCM: boolean = true; - private _historyProvider: ISCMHistoryProvider | undefined; + private readonly _historyProvider = observableValue(this, undefined); + get historyProvider() { return this._historyProvider; } constructor( private readonly proxy: ExtHostSCMShape, @@ -233,7 +270,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _providerId: string, private readonly _label: string, private readonly _rootUri: URI | undefined, - private readonly _inputBoxDocumentUri: URI, + private readonly _inputBoxTextModel: ITextModel, private readonly _quickDiffService: IQuickDiffService, private readonly _uriIdentService: IUriIdentityService, private readonly _workspaceContextService: IWorkspaceContextService @@ -253,11 +290,15 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._onDidChange.fire(); if (typeof features.commitTemplate !== 'undefined') { - this._onDidChangeCommitTemplate.fire(this.commitTemplate); + this._commitTemplate.set(features.commitTemplate, undefined); + } + + if (typeof features.count !== 'undefined') { + this._count.set(features.count, undefined); } if (typeof features.statusBarCommands !== 'undefined') { - this._onDidChangeStatusBarCommands.fire(this.statusBarCommands!); + this._statusBarCommands.set(features.statusBarCommands, undefined); } if (features.hasQuickDiffProvider && !this._quickDiff) { @@ -272,12 +313,11 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._quickDiff = undefined; } - if (features.hasHistoryProvider && !this._historyProvider) { - this._historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); - this._onDidChangeHistoryProvider.fire(); - } else if (features.hasHistoryProvider === false && this._historyProvider) { - this._historyProvider = undefined; - this._onDidChangeHistoryProvider.fire(); + if (features.hasHistoryProvider && !this.historyProvider.get()) { + const historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); + this._historyProvider.set(historyProvider, undefined); + } else if (features.hasHistoryProvider === false && this.historyProvider.get()) { + this._historyProvider.set(undefined, undefined); } } @@ -394,11 +434,11 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } $onDidChangeHistoryProviderCurrentHistoryItemGroup(currentHistoryItemGroup?: SCMHistoryItemGroupDto): void { - if (!this._historyProvider) { + if (!this.historyProvider.get()) { return; } - this._historyProvider.currentHistoryItemGroup = currentHistoryItemGroup ?? undefined; + this._historyProvider.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); } toJSON(): any { @@ -418,6 +458,7 @@ export class MainThreadSCM implements MainThreadSCMShape { private readonly _proxy: ExtHostSCMShape; private _repositories = new Map(); + private _repositoryBarriers = new Map(); private _repositoryDisposables = new Map(); private readonly _disposables = new DisposableStore(); @@ -425,11 +466,16 @@ export class MainThreadSCM implements MainThreadSCMShape { extHostContext: IExtHostContext, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, + @ITextModelService private readonly textModelService: ITextModelService, @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); + + this._disposables.add(new SCMInputBoxContentProvider(this.textModelService, this.modelService, this.languageService)); } dispose(): void { @@ -442,15 +488,20 @@ export class MainThreadSCM implements MainThreadSCMShape { this._disposables.dispose(); } - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService, this._uriIdentService, this.workspaceContextService); + async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise { + this._repositoryBarriers.set(handle, new Barrier()); + + const inputBoxTextModelRef = await this.textModelService.createModelReference(URI.revive(inputBoxDocumentUri)); + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, inputBoxTextModelRef.object.textEditorModel, this.quickDiffService, this._uriIdentService, this.workspaceContextService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); const disposable = combinedDisposable( + inputBoxTextModelRef, Event.filter(this.scmViewService.onDidFocusRepository, r => r === repository)(_ => this._proxy.$setSelectedSourceControl(handle)), repository.input.onDidChange(({ value }) => this._proxy.$onInputBoxValueChange(handle, value)) ); + this._repositoryDisposables.set(handle, disposable); if (this.scmViewService.focusedRepository === repository) { setTimeout(() => this._proxy.$setSelectedSourceControl(handle), 0); @@ -460,10 +511,11 @@ export class MainThreadSCM implements MainThreadSCMShape { setTimeout(() => this._proxy.$onInputBoxValueChange(handle, repository.input.value), 0); } - this._repositoryDisposables.set(handle, disposable); + this._repositoryBarriers.get(handle)?.open(); } - $updateSourceControl(handle: number, features: SCMProviderFeatures): void { + async $updateSourceControl(handle: number, features: SCMProviderFeatures): Promise { + await this._repositoryBarriers.get(handle)?.wait(); const repository = this._repositories.get(handle); if (!repository) { @@ -474,7 +526,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateSourceControl(features); } - $unregisterSourceControl(handle: number): void { + async $unregisterSourceControl(handle: number): Promise { + await this._repositoryBarriers.get(handle)?.wait(); const repository = this._repositories.get(handle); if (!repository) { @@ -488,7 +541,8 @@ export class MainThreadSCM implements MainThreadSCMShape { this._repositories.delete(handle); } - $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): void { + async $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -500,7 +554,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$spliceGroupResourceStates(splices); } - $updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): void { + async $updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -511,7 +566,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateGroup(groupHandle, features); } - $updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): void { + async $updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -522,7 +578,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateGroupLabel(groupHandle, label); } - $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): void { + async $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -533,7 +590,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$spliceGroupResourceStates(splices); } - $unregisterGroup(sourceControlHandle: number, handle: number): void { + async $unregisterGroup(sourceControlHandle: number, handle: number): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -544,7 +602,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$unregisterGroup(handle); } - $setInputBoxValue(sourceControlHandle: number, value: string): void { + async $setInputBoxValue(sourceControlHandle: number, value: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -554,7 +613,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.setValue(value, false); } - $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void { + async $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -564,7 +624,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.placeholder = placeholder; } - $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void { + async $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -574,7 +635,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.enabled = enabled; } - $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void { + async $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -584,7 +646,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.visible = visible; } - $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType) { + async $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { return; @@ -593,7 +656,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.showValidationMessage(message, type); } - $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void { + async $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -610,7 +674,8 @@ export class MainThreadSCM implements MainThreadSCMShape { } } - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void { + async $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { diff --git a/src/vs/workbench/api/browser/mainThreadSpeech.ts b/src/vs/workbench/api/browser/mainThreadSpeech.ts index 56ce1bca623..6dbb9033772 100644 --- a/src/vs/workbench/api/browser/mainThreadSpeech.ts +++ b/src/vs/workbench/api/browser/mainThreadSpeech.ts @@ -3,17 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { raceCancellation } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostContext, ExtHostSpeechShape, MainContext, MainThreadSpeechShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent } from 'vs/workbench/contrib/speech/common/speechService'; +import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent, ITextToSpeechEvent, TextToSpeechStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; type SpeechToTextSession = { readonly onDidChange: Emitter; }; +type TextToSpeechSession = { + readonly onDidChange: Emitter; +}; + type KeywordRecognitionSession = { readonly onDidChange: Emitter; }; @@ -26,6 +31,7 @@ export class MainThreadSpeech implements MainThreadSpeechShape { private readonly providerRegistrations = new Map(); private readonly speechToTextSessions = new Map(); + private readonly textToSpeechSessions = new Map(); private readonly keywordRecognitionSessions = new Map(); constructor( @@ -66,6 +72,36 @@ export class MainThreadSpeech implements MainThreadSpeechShape { onDidChange: onDidChange.event }; }, + createTextToSpeechSession: (token, options) => { + if (token.isCancellationRequested) { + return { + onDidChange: Event.None, + synthesize: async () => { } + }; + } + + const disposables = new DisposableStore(); + const session = Math.random(); + + this.proxy.$createTextToSpeechSession(handle, session, options?.language); + + const onDidChange = disposables.add(new Emitter()); + this.textToSpeechSessions.set(session, { onDidChange }); + + disposables.add(token.onCancellationRequested(() => { + this.proxy.$cancelTextToSpeechSession(session); + this.textToSpeechSessions.delete(session); + disposables.dispose(); + })); + + return { + onDidChange: onDidChange.event, + synthesize: async text => { + await this.proxy.$synthesizeSpeech(session, text); + await raceCancellation(Event.toPromise(Event.filter(onDidChange.event, e => e.status === TextToSpeechStatus.Stopped)), token); + } + }; + }, createKeywordRecognitionSession: token => { if (token.isCancellationRequested) { return { @@ -112,6 +148,11 @@ export class MainThreadSpeech implements MainThreadSpeechShape { providerSession?.onDidChange.fire(event); } + $emitTextToSpeechEvent(session: number, event: ITextToSpeechEvent): void { + const providerSession = this.textToSpeechSessions.get(session); + providerSession?.onDidChange.fire(event); + } + $emitKeywordRecognitionEvent(session: number, event: IKeywordRecognitionEvent): void { const providerSession = this.keywordRecognitionSessions.get(session); providerSession?.onDidChange.fire(event); @@ -124,6 +165,9 @@ export class MainThreadSpeech implements MainThreadSpeechShape { this.speechToTextSessions.forEach(session => session.onDidChange.dispose()); this.speechToTextSessions.clear(); + this.textToSpeechSessions.forEach(session => session.onDidChange.dispose()); + this.textToSpeechSessions.clear(); + this.keywordRecognitionSessions.forEach(session => session.onDidChange.dispose()); this.keywordRecognitionSessions.clear(); } diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 8fe9a431321..9c37c597978 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -10,7 +10,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import * as Types from 'vs/base/common/types'; import * as Platform from 'vs/base/common/platform'; import { IStringDictionary } from 'vs/base/common/collections'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -414,7 +414,7 @@ namespace TaskFilterDTO { } @extHostNamedCustomer(MainContext.MainThreadTask) -export class MainThreadTask implements MainThreadTaskShape { +export class MainThreadTask extends Disposable implements MainThreadTaskShape { private readonly _extHostContext: IExtHostContext | undefined; private readonly _proxy: ExtHostTaskShape; @@ -426,9 +426,10 @@ export class MainThreadTask implements MainThreadTaskShape { @IWorkspaceContextService private readonly _workspaceContextServer: IWorkspaceContextService, @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService ) { + super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask); this._providers = new Map(); - this._taskService.onDidStateChange(async (event: ITaskEvent) => { + this._register(this._taskService.onDidStateChange(async (event: ITaskEvent) => { if (event.kind === TaskEventKind.Changed) { return; } @@ -453,14 +454,15 @@ export class MainThreadTask implements MainThreadTaskShape { } else if (event.kind === TaskEventKind.End) { this._proxy.$OnDidEndTask(TaskExecutionDTO.from(task.getTaskExecution())); } - }); + })); } - public dispose(): void { + public override dispose(): void { for (const value of this._providers.values()) { value.disposable.dispose(); } this._providers.clear(); + super.dispose(); } $createTaskId(taskDTO: ITaskDTO): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index cc08f3cdf7c..663c3fa5728 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -49,7 +49,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape * provided through this, even from multiple ext link providers. Xterm should remove lower * priority intersecting links itself. */ - private _linkProvider = this._store.add(new MutableDisposable()); + private readonly _linkProvider = this._store.add(new MutableDisposable()); private _os: OperatingSystem = OS; diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index 97eefc8b00e..3429833aa9c 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable, type IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { TerminalCapability, type ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ExtHostContext, MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { extHostNamedCustomer, type IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { TerminalShellExecutionCommandLineConfidence } from 'vs/workbench/api/common/extHostTypes'; @extHostNamedCustomer(MainContext.MainThreadTerminalShellIntegration) export class MainThreadTerminalShellIntegration extends Disposable implements MainThreadTerminalShellIntegrationShape { @@ -24,14 +26,21 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalShellIntegration); + const instanceDataListeners: Map = new Map(); + this._register(toDisposable(() => { + for (const listener of instanceDataListeners.values()) { + listener.dispose(); + } + })); + // onDidChangeTerminalShellIntegration - const onDidAddCommandDetection = this._terminalService.createOnInstanceEvent(instance => { + const onDidAddCommandDetection = this._store.add(this._terminalService.createOnInstanceEvent(instance => { return Event.map( Event.filter(instance.capabilities.onDidAddCapabilityType, e => { return e === TerminalCapability.CommandDetection; - }, this._store), () => instance + }), () => instance ); - }); + })).event; this._store.add(onDidAddCommandDetection(e => this._proxy.$shellIntegrationChange(e.instanceId))); // onDidStartTerminalShellExecution @@ -43,32 +52,56 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma if (e.data === currentCommand) { return; } + // String paths are not exposed in the extension API currentCommand = e.data; - this._proxy.$shellExecutionStart(e.instance.instanceId, e.data.command, e.data.cwd); + const instanceId = e.instance.instanceId; + this._proxy.$shellExecutionStart(instanceId, e.data.command, convertToExtHostCommandLineConfidence(e.data), e.data.isTrusted, this._convertCwdToUri(e.data.cwd)); + + // TerminalShellExecution.createDataStream + // Debounce events to reduce the message count - when this listener is disposed the events will be flushed + instanceDataListeners.get(instanceId)?.dispose(); + instanceDataListeners.set(instanceId, Event.accumulate(e.instance.onData, 50, this._store)(events => this._proxy.$shellExecutionData(instanceId, events.join('')))); })); // onDidEndTerminalShellExecution const commandDetectionEndEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandFinished)); this._store.add(commandDetectionEndEvent.event(e => { currentCommand = undefined; - this._proxy.$shellExecutionEnd(e.instance.instanceId, e.data.command, e.data.exitCode); + const instanceId = e.instance.instanceId; + instanceDataListeners.get(instanceId)?.dispose(); + // Send end in a microtask to ensure the data events are sent first + setTimeout(() => { + this._proxy.$shellExecutionEnd(instanceId, e.data.command, convertToExtHostCommandLineConfidence(e.data), e.data.isTrusted, e.data.exitCode); + }); })); // onDidChangeTerminalShellIntegration via cwd const cwdChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CwdDetection, e => e.onDidChangeCwd)); - this._store.add(cwdChangeEvent.event(e => this._proxy.$cwdChange(e.instance.instanceId, e.data))); + this._store.add(cwdChangeEvent.event(e => { + this._proxy.$cwdChange(e.instance.instanceId, this._convertCwdToUri(e.data)); + })); // Clean up after dispose this._store.add(this._terminalService.onDidDisposeInstance(e => this._proxy.$closeTerminal(e.instanceId))); - - // TerminalShellExecution.createDataStream - // TODO: Support this on remote; it should go via the server - if (!workbenchEnvironmentService.remoteAuthority) { - this._store.add(this._terminalService.onAnyInstanceData(e => this._proxy.$shellExecutionData(e.instance.instanceId, e.data))); - } } $executeCommand(terminalId: number, commandLine: string): void { this._terminalService.getInstanceFromId(terminalId)?.runCommand(commandLine, true); } + + private _convertCwdToUri(cwd: string | undefined): URI | undefined { + return cwd ? URI.file(cwd) : undefined; + } +} + +function convertToExtHostCommandLineConfidence(command: ITerminalCommand): TerminalShellExecutionCommandLineConfidence { + switch (command.commandLineConfidence) { + case 'high': + return TerminalShellExecutionCommandLineConfidence.High; + case 'medium': + return TerminalShellExecutionCommandLineConfidence.Medium; + case 'low': + default: + return TerminalShellExecutionCommandLineConfidence.Low; + } } diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index a316bec0eb3..5f5a2afc39e 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -7,30 +7,29 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ISettableObservable, transaction } from 'vs/base/common/observable'; +import { ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestControllerCapability, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadTesting) -export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider { +export class MainThreadTesting extends Disposable implements MainThreadTestingShape { private readonly proxy: ExtHostTestingShape; private readonly diffListener = this._register(new MutableDisposable()); private readonly testProviderRegistrations = new Map; - canRefresh: MutableObservableValue; + label: ISettableObservable; + capabilities: ISettableObservable; disposable: IDisposable; }>(); @@ -44,8 +43,15 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh super(); this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting); - this._register(this.testService.onDidCancelTestRun(({ runId }) => { - this.proxy.$cancelExtensionTestRun(runId); + this._register(this.testService.registerExtHost({ + provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token), + executeTestFollowup: id => this.proxy.$executeTestFollowup(id), + disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids), + getTestsRelatedToCode: (uri, position, token) => this.proxy.$getTestsRelatedToCode(uri, position, token), + })); + + this._register(this.testService.onDidCancelTestRun(({ runId, taskId }) => { + this.proxy.$cancelExtensionTestRun(runId, taskId); })); this._register(Event.debounce(testProfiles.onDidChange, (_last, e) => e)(() => { @@ -143,8 +149,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh transaction(tx => { let value = task.coverage.read(undefined); if (!value) { - value = new TestCoverage(taskId, this.uriIdentityService, { - getCoverageDetails: (id, token) => this.proxy.$getCoverageDetails(id, token) + value = new TestCoverage(run, taskId, this.uriIdentityService, { + getCoverageDetails: (id, testId, token) => this.proxy.$getCoverageDetails(id, testId, token) .then(r => r.map(CoverageDetails.deserialize)), }); value.append(deserialized, tx); @@ -219,20 +225,26 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - public $registerTestController(controllerId: string, labelStr: string, canRefreshValue: boolean) { + public $registerTestController(controllerId: string, _label: string, _capabilities: TestControllerCapability) { const disposable = new DisposableStore(); - const label = disposable.add(new MutableObservableValue(labelStr)); - const canRefresh = disposable.add(new MutableObservableValue(canRefreshValue)); + const label = observableValue(`${controllerId}.label`, _label); + const capabilities = observableValue(`${controllerId}.cap`, _capabilities); const controller: IMainThreadTestController = { id: controllerId, label, - canRefresh, + capabilities, syncTests: () => this.proxy.$syncTests(), refreshTests: token => this.proxy.$refreshTests(controllerId, token), configureRunProfile: id => this.proxy.$configureRunProfile(controllerId, id), runTests: (reqs, token) => this.proxy.$runControllerTests(reqs, token), startContinuousRun: (reqs, token) => this.proxy.$startContinuousRun(reqs, token), expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1), + getRelatedCode: (testId, token) => this.proxy.$getCodeRelatedToTest(testId, token).then(locations => + locations.map(l => ({ + uri: URI.revive(l.uri), + range: Range.lift(l.range) + })), + ), }; disposable.add(toDisposable(() => this.testProfiles.removeProfile(controllerId))); @@ -241,7 +253,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh this.testProviderRegistrations.set(controllerId, { instance: controller, label, - canRefresh, + capabilities, disposable }); } @@ -255,13 +267,16 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh return; } - if (patch.label !== undefined) { - controller.label.value = patch.label; - } + transaction(tx => { + if (patch.label !== undefined) { + controller.label.set(patch.label, tx); + } + + if (patch.capabilities !== undefined) { + controller.capabilities.set(patch.capabilities, tx); + } + }); - if (patch.canRefresh !== undefined) { - controller.canRefresh.value = patch.canRefresh; - } } /** diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index 4fb13ab6231..5b8cf2637fc 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { MainThreadTunnelServiceShape, MainContext, ExtHostContext, ExtHostTunnelServiceShape, CandidatePortSource, PortAttributesSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol'; import { TunnelDtoConverter } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IRemoteExplorerService, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_HYBRID, PORT_AUTO_SOURCE_SETTING_OUTPUT } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, ProvidedPortAttributes, PortAttributesProvider, TunnelProtocol } from 'vs/platform/tunnel/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; @@ -223,6 +223,11 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); break; } + case CandidatePortSource.Hybrid: { + Registry.as(ConfigurationExtensions.Configuration) + .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_HYBRID } }]); + break; + } default: // Do nothing, the defaults for these settings should be used. } }).catch(() => { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 180932b6d6d..12441286ddc 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -14,7 +14,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IRequestService } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -223,6 +223,14 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return this._requestService.resolveProxy(url); } + $lookupAuthorization(authInfo: AuthInfo): Promise { + return this._requestService.lookupAuthorization(authInfo); + } + + $lookupKerberosAuthorization(url: string): Promise { + return this._requestService.lookupKerberosAuthorization(url); + } + $loadCertificates(): Promise { return this._requestService.loadCertificates(); } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index fe77f8325b3..b28bd289ca6 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -11,14 +11,14 @@ import { localize } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { Extensions as ViewletExtensions, PaneCompositeRegistry } from 'vs/workbench/browser/panecomposite'; -import { CustomTreeView, RawCustomTreeViewContextKey, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; -import { Extensions as ViewContainerExtensions, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ResolvableTreeItem, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; @@ -26,17 +26,10 @@ import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions as ExtensionFeaturesRegistryExtensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownString } from 'vs/base/common/htmlContent'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -104,6 +97,8 @@ interface IUserFriendlyViewDescriptor { group?: string; remoteName?: string | string[]; virtualWorkspace?: string; + + accessibilityHelpContent?: string; } enum InitialVisibility { @@ -167,6 +162,10 @@ const viewDescriptor: IJSONSchema = { initialSize: { type: 'number', description: localize('vscode.extension.contributs.view.size', "The initial size of the view. The size will behave like the css 'flex' property, and will set the initial size when the view is first shown. In the side bar, this is the height of the view. This value is only respected when the same extension owns both the view and the view container."), + }, + accessibilityHelpContent: { + type: 'string', + markdownDescription: localize('vscode.extension.contributes.view.accessibilityHelpContent', "When the accessibility help dialog is invoked in this view, this content will be presented to the user as a markdown string. Keybindings will be resolved when provided in the format of . If there is no keybinding, that will be indicated and this command will be included in a quickpick for easy configuration.") } } }; @@ -284,51 +283,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { this.viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); this.handleAndRegisterCustomViewContainers(); this.handleAndRegisterCustomViews(); - - let showTreeHoverCancellation = new CancellationTokenSource(); - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.showTreeHover', - handler: async (accessor: ServicesAccessor, ...args: any[]) => { - showTreeHoverCancellation.cancel(); - showTreeHoverCancellation = new CancellationTokenSource(); - const listService = accessor.get(IListService); - const treeViewsService = accessor.get(ITreeViewsService); - const hoverService = accessor.get(IHoverService); - const lastFocusedList = listService.lastFocusedList; - if (!(lastFocusedList instanceof AsyncDataTree)) { - return; - } - const focus = lastFocusedList.getFocus(); - if (!focus || (focus.length === 0)) { - return; - } - const treeItem = focus[0]; - - if (treeItem instanceof ResolvableTreeItem) { - await treeItem.resolve(showTreeHoverCancellation.token); - } - if (!treeItem.tooltip) { - return; - } - const element = treeViewsService.getRenderedTreeElement(('handle' in treeItem) ? treeItem.handle : treeItem); - if (!element) { - return; - } - hoverService.showHover({ - content: treeItem.tooltip, - target: element, - position: { - hoverPosition: HoverPosition.BELOW, - }, - persistence: { - hideOnHover: false - } - }, true); - }, - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI), - when: ContextKeyExpr.and(RawCustomTreeViewContextKey, WorkbenchListFocusContextKey) - }); } private handleAndRegisterCustomViewContainers() { @@ -538,6 +492,11 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } } + let accessibilityHelpContent; + if (isProposedApiEnabled(extension.description, 'contribAccessibilityHelpContent') && item.accessibilityHelpContent) { + accessibilityHelpContent = new MarkdownString(item.accessibilityHelpContent); + } + const viewDescriptor: ICustomViewDescriptor = { type: type, ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane), @@ -558,7 +517,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { virtualWorkspace: item.virtualWorkspace, hideByDefault: initialVisibility === InitialVisibility.Hidden, workspace: viewContainer?.id === REMOTE ? true : undefined, - weight + weight, + accessibilityHelpContent }; diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 496651fe769..8b7c408c766 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects'; import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IConfigurationNode, IConfigurationRegistry, Extensions, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_REGEX, IConfigurationDefaults, configurationDefaultsSchemaId, IConfigurationDelta, getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationNode, IConfigurationRegistry, Extensions, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_REGEX, IConfigurationDefaults, configurationDefaultsSchemaId, IConfigurationDelta, getDefaultValue, getAllConfigurationProperties, parseScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { workspaceSettingsSchemaId, launchSchemaId, tasksSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { isObject, isUndefined } from 'vs/base/common/types'; @@ -160,11 +160,17 @@ defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => { const addedDefaultConfigurations = added.map(extension => { const overrides: IStringDictionary = objects.deepClone(extension.value); for (const key of Object.keys(overrides)) { + const registeredPropertyScheme = registeredProperties[key]; + if (registeredPropertyScheme?.disallowConfigurationDefault) { + extension.collector.warn(nls.localize('config.property.preventDefaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. This setting does not allow contributing configuration defaults.", key)); + delete overrides[key]; + continue; + } if (!OVERRIDE_PROPERTY_REGEX.test(key)) { - const registeredPropertyScheme = registeredProperties[key]; if (registeredPropertyScheme?.scope && !allowedScopes.includes(registeredPropertyScheme.scope)) { extension.collector.warn(nls.localize('config.property.defaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. Only defaults for machine-overridable, window, resource and language overridable scoped settings are supported.", key)); delete overrides[key]; + continue; } } } @@ -210,8 +216,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { const seenProperties = new Set(); - function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser): IConfigurationNode[] { - const configurations: IConfigurationNode[] = []; + function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser): IConfigurationNode { const configuration = objects.deepClone(node); if (configuration.title && (typeof configuration.title !== 'string')) { @@ -224,8 +229,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { configuration.extensionInfo = { id: extension.description.identifier.value, displayName: extension.description.displayName }; configuration.restrictedProperties = extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined; configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value; - configurations.push(configuration); - return configurations; + return configuration; } function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser): void { @@ -254,23 +258,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { continue; } seenProperties.add(key); - if (propertyConfiguration.scope) { - if (propertyConfiguration.scope.toString() === 'application') { - propertyConfiguration.scope = ConfigurationScope.APPLICATION; - } else if (propertyConfiguration.scope.toString() === 'machine') { - propertyConfiguration.scope = ConfigurationScope.MACHINE; - } else if (propertyConfiguration.scope.toString() === 'resource') { - propertyConfiguration.scope = ConfigurationScope.RESOURCE; - } else if (propertyConfiguration.scope.toString() === 'machine-overridable') { - propertyConfiguration.scope = ConfigurationScope.MACHINE_OVERRIDABLE; - } else if (propertyConfiguration.scope.toString() === 'language-overridable') { - propertyConfiguration.scope = ConfigurationScope.LANGUAGE_OVERRIDABLE; - } else { - propertyConfiguration.scope = ConfigurationScope.WINDOW; - } - } else { - propertyConfiguration.scope = ConfigurationScope.WINDOW; - } + propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW; } } const subNodes = configuration.allOf; @@ -288,9 +276,9 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { const configurations: IConfigurationNode[] = []; const value = extension.value; if (Array.isArray(value)) { - value.forEach(v => configurations.push(...handleConfiguration(v, extension))); + value.forEach(v => configurations.push(handleConfiguration(v, extension))); } else { - configurations.push(...handleConfiguration(value, extension)); + configurations.push(handleConfiguration(value, extension)); } extensionConfigurations.set(extension.description.identifier, configurations); addedConfigurations.push(...configurations); @@ -400,15 +388,11 @@ class SettingsTableRenderer extends Disposable implements IExtensionFeatureTable } render(manifest: IExtensionManifest): IRenderedData { - const configuration = manifest.contributes?.configuration; - let properties: any = {}; - if (Array.isArray(configuration)) { - configuration.forEach(config => { - properties = { ...properties, ...config.properties }; - }); - } else if (configuration) { - properties = configuration.properties; - } + const configuration: IConfigurationNode[] = manifest.contributes?.configuration + ? Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration] + : []; + + const properties = getAllConfigurationProperties(configuration); const contrib = properties ? Object.keys(properties) : []; const headers = [nls.localize('setting name', "ID"), nls.localize('description', "Description"), nls.localize('default', "Default")]; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 7f9eae154c3..43c5387670b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -27,9 +28,7 @@ import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; -import { ExtHostChat } from 'vs/workbench/api/common/extHostChat'; import { ExtHostChatAgents2 } from 'vs/workbench/api/common/extHostChatAgents2'; -import { IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { ExtHostChatVariables } from 'vs/workbench/api/common/extHostChatVariables'; import { ExtHostClipboard } from 'vs/workbench/api/common/extHostClipboard'; import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets'; @@ -46,6 +45,7 @@ import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostD import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { ExtHostEmbeddings } from 'vs/workbench/api/common/extHostEmbedding'; import { ExtHostAiEmbeddingVector } from 'vs/workbench/api/common/extHostEmbeddingVector'; import { Extension, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostFileSystem } from 'vs/workbench/api/common/extHostFileSystem'; @@ -53,11 +53,11 @@ import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileS import { ExtHostFileSystemEventService, FileSystemWatcherCreateOptions } from 'vs/workbench/api/common/extHostFileSystemEventService'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; -import { ExtHostInteractiveEditor } from 'vs/workbench/api/common/extHostInlineChat'; import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; -import { ExtHostIssueReporter } from 'vs/workbench/api/common/extHostIssueReporter'; import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtHostLanguageModelTools } from 'vs/workbench/api/common/extHostLanguageModelTools'; +import { IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages'; import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService'; import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets'; @@ -85,7 +85,8 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import { IExtHostTask } from 'vs/workbench/api/common/extHostTask'; import { ExtHostTelemetryLogger, IExtHostTelemetry, isNewAppInstall } from 'vs/workbench/api/common/extHostTelemetry'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; -import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; +import { IExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; @@ -106,9 +107,8 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import { UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; +import { ExcludeSettingOptions, oldToNewTextSearchResult, TextSearchCompleteMessageType, TextSearchCompleteMessageTypeNew, TextSearchContextNew, TextSearchMatchNew } from 'vs/workbench/services/search/common/searchExtTypes'; import type * as vscode from 'vscode'; -import { IExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -180,7 +180,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, extHostLogService)); const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); @@ -207,19 +207,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); - const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostLogService, extHostCommands, extHostDocumentsAndEditors)); + const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, accessor.get(IExtHostTesting)); const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); - const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInlineChat, new ExtHostInteractiveEditor(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService)); - const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands)); + const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); - const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol)); + const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); - const extHostIssueReporter = rpcProtocol.set(ExtHostContext.ExtHostIssueReporter, new ExtHostIssueReporter(rpcProtocol)); const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol)); + const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol)); // Check that no named customers are missing const expected = Object.values>(ExtHostContext); @@ -293,9 +292,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, - getSessions(providerId: string, scopes: readonly string[]) { - checkProposedApiEnabled(extension, 'authGetSessions'); - return extHostAuthentication.getSessions(extension, providerId, scopes); + getAccounts(providerId: string) { + return extHostAuthentication.getAccounts(providerId); }, // TODO: remove this after GHPR and Codespaces move off of it async hasSession(providerId: string, scopes: readonly string[]) { @@ -430,14 +428,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeLogLevel() { return _asExtensionEvent(extHostLogService.onDidChangeLogLevel); }, - registerIssueUriRequestHandler(handler: vscode.IssueUriRequestHandler) { - checkProposedApiEnabled(extension, 'handleIssueUri'); - return extHostIssueReporter.registerIssueUriRequestHandler(extension, handler); - }, - registerIssueDataProvider(handler: vscode.IssueDataProvider) { - checkProposedApiEnabled(extension, 'handleIssueUri'); - return extHostIssueReporter.registerIssueDataProvider(extension, handler); - }, get appQuality(): string | undefined { checkProposedApiEnabled(extension, 'resolvers'); return initData.quality; @@ -465,6 +455,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'testObserver'); return extHostTesting.runTests(provider); }, + registerTestFollowupProvider(provider) { + checkProposedApiEnabled(extension, 'testObserver'); + return extHostTesting.registerTestFollowupProvider(provider); + }, get onDidChangeTestResults() { checkProposedApiEnabled(extension, 'testObserver'); return _asExtensionEvent(extHostTesting.onResultsChanged); @@ -675,7 +669,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguages.createLanguageStatusItem(extension, id, selector); }, registerDocumentDropEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentDropEditProvider, metadata?: vscode.DocumentDropEditProviderMetadata): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentOnDropEditProvider(extension, selector, provider, isProposedApiEnabled(extension, 'dropMetadata') ? metadata : undefined); + return extHostLanguageFeatures.registerDocumentOnDropEditProvider(extension, selector, provider, isProposedApiEnabled(extension, 'documentPaste') ? metadata : undefined); } }; @@ -749,19 +743,16 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return _asExtensionEvent(extHostTerminalService.onDidExecuteTerminalCommand)(listener, thisArg, disposables); }, onDidChangeTerminalShellIntegration(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'terminalShellIntegration'); return _asExtensionEvent(extHostTerminalShellIntegration.onDidChangeTerminalShellIntegration)(listener, thisArg, disposables); }, onDidStartTerminalShellExecution(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'terminalShellIntegration'); return _asExtensionEvent(extHostTerminalShellIntegration.onDidStartTerminalShellExecution)(listener, thisArg, disposables); }, onDidEndTerminalShellExecution(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'terminalShellIntegration'); return _asExtensionEvent(extHostTerminalShellIntegration.onDidEndTerminalShellExecution)(listener, thisArg, disposables); }, get state() { - return extHostWindow.getState(extension); + return extHostWindow.getState(); }, onDidChangeWindowState(listener, thisArg?, disposables?) { return _asExtensionEvent(extHostWindow.onDidChangeWindowState)(listener, thisArg, disposables); @@ -969,10 +960,25 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // Note, undefined/null have different meanings on "exclude" return extHostWorkspace.findFiles(include, exclude, maxResults, extension.identifier, token); }, - findFiles2: (filePattern, options?, token?) => { + findFiles2: (filePattern: vscode.GlobPattern, options?: vscode.FindFiles2Options, token?: vscode.CancellationToken): Thenable => { checkProposedApiEnabled(extension, 'findFiles2'); return extHostWorkspace.findFiles2(filePattern, options, extension.identifier, token); }, + findFiles2New: (filePattern: vscode.GlobPattern[], options?: vscode.FindFiles2OptionsNew, token?: vscode.CancellationToken): Thenable => { + checkProposedApiEnabled(extension, 'findFiles2New'); + + const oldOptions = { + exclude: options?.exclude && options.exclude.length > 0 ? options.exclude[0] : undefined, + useDefaultExcludes: !options?.useExcludeSettings || (options?.useExcludeSettings === ExcludeSettingOptions.FilesExclude || options?.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + useDefaultSearchExcludes: !options?.useExcludeSettings || (options?.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + maxResults: options?.maxResults, + useIgnoreFiles: options?.useIgnoreFiles?.local, + useGlobalIgnoreFiles: options?.useIgnoreFiles?.global, + useParentIgnoreFiles: options?.useIgnoreFiles?.parent, + followSymlinks: options?.followSymlinks, + }; + return extHostWorkspace.findFiles2(filePattern && filePattern.length > 0 ? filePattern[0] : undefined, oldOptions, extension.identifier, token); + }, findTextInFiles: (query: vscode.TextSearchQuery, optionsOrCallback: vscode.FindTextInFilesOptions | ((result: vscode.TextSearchResult) => void), callbackOrToken?: vscode.CancellationToken | ((result: vscode.TextSearchResult) => void), token?: vscode.CancellationToken) => { checkProposedApiEnabled(extension, 'findTextInFiles'); let options: vscode.FindTextInFilesOptions; @@ -989,6 +995,60 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostWorkspace.findTextInFiles(query, options || {}, callback, extension.identifier, token); }, + findTextInFilesNew: (query: vscode.TextSearchQueryNew, options?: vscode.FindTextInFilesOptionsNew, token?: vscode.CancellationToken): vscode.FindTextInFilesResponse => { + checkProposedApiEnabled(extension, 'findTextInFilesNew'); + checkProposedApiEnabled(extension, 'textSearchProviderNew'); + let oldOptions = {}; + + + if (options) { + oldOptions = { + include: options.include && options.include.length > 0 ? options.include[0] : undefined, + exclude: options.exclude && options.exclude.length > 0 ? options.exclude[0] : undefined, + useDefaultExcludes: options.useExcludeSettings === undefined || (options.useExcludeSettings === ExcludeSettingOptions.FilesExclude || options.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + useSearchExclude: options.useExcludeSettings === undefined || (options.useExcludeSettings === ExcludeSettingOptions.SearchAndFilesExclude), + maxResults: options.maxResults, + useIgnoreFiles: options.useIgnoreFiles?.local, + useGlobalIgnoreFiles: options.useIgnoreFiles?.global, + useParentIgnoreFiles: options.useIgnoreFiles?.parent, + followSymlinks: options.followSymlinks, + encoding: options.encoding, + previewOptions: options.previewOptions ? { + matchLines: options.previewOptions?.numMatchLines ?? 100, + charsPerLine: options.previewOptions?.charsPerLine ?? 10000, + } : undefined, + beforeContext: options.surroundingContext, + afterContext: options.surroundingContext, + } satisfies vscode.FindTextInFilesOptions & { useSearchExclude?: boolean }; + } + + const complete: Promise = Promise.resolve(undefined); + + const asyncIterable = new AsyncIterableObject(async emitter => { + const callback = async (result: vscode.TextSearchResult) => { + emitter.emitOne(oldToNewTextSearchResult(result)); + return result; + }; + await complete.then(e => { + return extHostWorkspace.findTextInFiles( + query, + oldOptions, + callback, + extension.identifier, + token + ); + }); + }); + + return { + results: asyncIterable, + complete: complete.then((e) => { + return { + limitHit: e?.limitHit ?? false + }; + }), + }; + }, save: (uri) => { return extHostWorkspace.save(uri); }, @@ -1042,6 +1102,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return uriPromise.then(uri => { + extHostLogService.trace(`openTextDocument from ${extension.identifier}`); if (uri.scheme === Schemas.vscodeRemote && !uri.authority) { extHostApiDeprecation.report('workspace.openTextDocument', extension, `A URI of 'vscode-remote' scheme requires an authority.`); } @@ -1137,6 +1198,20 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'textSearchProvider'); return extHostSearch.registerAITextSearchProvider(scheme, provider); }, + registerFileSearchProviderNew: (scheme: string, provider: vscode.FileSearchProviderNew) => { + checkProposedApiEnabled(extension, 'fileSearchProviderNew'); + return { dispose: () => { } }; + }, + registerTextSearchProviderNew: (scheme: string, provider: vscode.TextSearchProviderNew) => { + checkProposedApiEnabled(extension, 'textSearchProviderNew'); + return { dispose: () => { } }; + }, + registerAITextSearchProviderNew: (scheme: string, provider: vscode.AITextSearchProviderNew) => { + // there are some dependencies on textSearchProvider, so we need to check for both + checkProposedApiEnabled(extension, 'aiTextSearchProviderNew'); + checkProposedApiEnabled(extension, 'textSearchProviderNew'); + return { dispose: () => { } }; + }, registerRemoteAuthorityResolver: (authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver) => { checkProposedApiEnabled(extension, 'resolvers'); return extensionService.registerRemoteAuthorityResolver(authorityPrefix, resolver); @@ -1256,9 +1331,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostDebugService.breakpoints; }, get activeStackItem() { - if (!isProposedApiEnabled(extension, 'debugFocus')) { - return undefined; - } return extHostDebugService.activeStackItem; }, registerDebugVisualizationProvider(id, provider) { @@ -1285,7 +1357,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return _asExtensionEvent(extHostDebugService.onDidChangeBreakpoints)(listener, thisArgs, disposables); }, onDidChangeActiveStackItem(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'debugFocus'); return _asExtensionEvent(extHostDebugService.onDidChangeActiveStackItem)(listener, thisArg, disposables); }, registerDebugConfigurationProvider(debugType: string, provider: vscode.DebugConfigurationProvider, triggerKind?: vscode.DebugConfigurationProviderTriggerKind) { @@ -1393,21 +1464,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: interactive const interactive: typeof vscode.interactive = { - // IMPORTANT - // this needs to be updated whenever the API proposal changes + // TODO Can be deleted after another Insiders _version: 1, - - registerInteractiveEditorSessionProvider(provider: vscode.InteractiveEditorSessionProvider, metadata?: vscode.InteractiveEditorSessionProviderMetadata) { + transferActiveChat(toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); - return extHostInteractiveEditor.registerProvider(extension, provider, metadata); - }, - registerInteractiveSessionProvider(id: string, provider: vscode.InteractiveSessionProvider) { - checkProposedApiEnabled(extension, 'interactive'); - return extHostChat.registerChatProvider(extension, id, provider); - }, - transferChatSession(session: vscode.InteractiveSession, toWorkspace: vscode.Uri) { - checkProposedApiEnabled(extension, 'interactive'); - return extHostChat.transferChatSession(session, toWorkspace); + return extHostChatAgents2.transferActiveChat(toWorkspace); } }; @@ -1429,42 +1490,80 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: chat const chat: typeof vscode.chat = { + // IMPORTANT + // this needs to be updated whenever the API proposal changes and breaks backwards compatibility + _version: 1, + registerChatResponseProvider(id: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata) { checkProposedApiEnabled(extension, 'chatProvider'); return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); }, - registerChatVariableResolver(name: string, description: string, resolver: vscode.ChatVariableResolver) { + registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, icon?: vscode.ThemeIcon) { checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.registerVariableResolver(extension, name, description, resolver); + return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver, fullName, icon?.id); }, registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); }, createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) { - checkProposedApiEnabled(extension, 'chatParticipant'); return extHostChatAgents2.createChatAgent(extension, id, handler); }, - createDynamicChatParticipant(id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + createDynamicChatParticipant(id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); + }, + registerChatParticipantDetectionProvider(provider: vscode.ChatParticipantDetectionProvider) { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return extHostChatAgents2.createDynamicChatAgent(extension, id, name, description, handler); - } + return extHostChatAgents2.registerChatParticipantDetectionProvider(provider); + }, }; // namespace: lm const lm: typeof vscode.lm = { - get languageModels() { - checkProposedApiEnabled(extension, 'languageModels'); - return extHostLanguageModels.getLanguageModelIds(); + selectChatModels: (selector) => { + return extHostLanguageModels.selectLanguageModels(extension, selector ?? {}); }, - onDidChangeLanguageModels: (listener, thisArgs?, disposables?) => { - checkProposedApiEnabled(extension, 'languageModels'); + onDidChangeChatModels: (listener, thisArgs?, disposables?) => { return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, - sendChatRequest(languageModel: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: vscode.CancellationToken) { - checkProposedApiEnabled(extension, 'languageModels'); - return extHostLanguageModels.sendChatRequest(extension, languageModel, messages, options, token); - } + registerChatModelProvider: (id, provider, metadata) => { + checkProposedApiEnabled(extension, 'chatProvider'); + return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); + }, + // --- embeddings + get embeddingModels() { + checkProposedApiEnabled(extension, 'embeddings'); + return extHostEmbeddings.embeddingsModels; + }, + onDidChangeEmbeddingModels: (listener, thisArgs?, disposables?) => { + checkProposedApiEnabled(extension, 'embeddings'); + return extHostEmbeddings.onDidChange(listener, thisArgs, disposables); + }, + registerEmbeddingsProvider(embeddingsModel, provider) { + checkProposedApiEnabled(extension, 'embeddings'); + return extHostEmbeddings.registerEmbeddingsProvider(extension, embeddingsModel, provider); + }, + async computeEmbeddings(embeddingsModel, input, token?): Promise { + checkProposedApiEnabled(extension, 'embeddings'); + if (typeof input === 'string') { + return extHostEmbeddings.computeEmbeddings(embeddingsModel, input, token); + } else { + return extHostEmbeddings.computeEmbeddings(embeddingsModel, input, token); + } + }, + registerTool(toolId: string, tool: vscode.LanguageModelTool) { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.registerTool(extension, toolId, tool); + }, + invokeTool(toolId: string, parameters: Object, token: vscode.CancellationToken) { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.invokeTool(toolId, parameters, token); + }, + get tools() { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.tools; + }, }; // namespace: speech @@ -1522,6 +1621,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, CommentThreadState: extHostTypes.CommentThreadState, CommentThreadApplicability: extHostTypes.CommentThreadApplicability, + CommentThreadFocus: extHostTypes.CommentThreadFocus, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionItemTag: extHostTypes.CompletionItemTag, @@ -1570,6 +1670,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlineCompletionItem: extHostTypes.InlineSuggestion, InlineCompletionList: extHostTypes.InlineSuggestionList, Hover: extHostTypes.Hover, + VerboseHover: extHostTypes.VerboseHover, + HoverVerbosityAction: extHostTypes.HoverVerbosityAction, IndentAction: languageConfiguration.IndentAction, Location: extHostTypes.Location, MarkdownString: extHostTypes.MarkdownString, @@ -1579,6 +1681,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I Position: extHostTypes.Position, ProcessExecution: extHostTypes.ProcessExecution, ProgressLocation: extHostTypes.ProgressLocation, + QuickInputButtonLocation: extHostTypes.QuickInputButtonLocation, QuickInputButtons: extHostTypes.QuickInputButtons, Range: extHostTypes.Range, RelativePattern: extHostTypes.RelativePattern, @@ -1612,6 +1715,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalLocation: extHostTypes.TerminalLocation, TerminalProfile: extHostTypes.TerminalProfile, TerminalExitReason: extHostTypes.TerminalExitReason, + TerminalShellExecutionCommandLineConfidence: extHostTypes.TerminalShellExecutionCommandLineConfidence, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, @@ -1634,7 +1738,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // proposed api types DocumentPasteTriggerKind: extHostTypes.DocumentPasteTriggerKind, DocumentDropEdit: extHostTypes.DocumentDropEdit, - DocumentPasteEditKind: extHostTypes.DocumentPasteEditKind, + DocumentDropOrPasteEditKind: extHostTypes.DocumentDropOrPasteEditKind, DocumentPasteEdit: extHostTypes.DocumentPasteEdit, InlayHint: extHostTypes.InlayHint, InlayHintLabelPart: extHostTypes.InlayHintLabelPart, @@ -1666,6 +1770,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TestResultState: extHostTypes.TestResultState, TestRunRequest: extHostTypes.TestRunRequest, TestMessage: extHostTypes.TestMessage, + TestMessage2: extHostTypes.TestMessage, + TestMessageStackFrame: extHostTypes.TestMessageStackFrame, TestTag: extHostTypes.TestTag, TestRunProfileKind: extHostTypes.TestRunProfileKind, TextSearchCompleteMessageType: TextSearchCompleteMessageType, @@ -1673,10 +1779,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I DataTransferItem: extHostTypes.DataTransferItem, TestCoverageCount: extHostTypes.TestCoverageCount, FileCoverage: extHostTypes.FileCoverage, + FileCoverage2: extHostTypes.FileCoverage, StatementCoverage: extHostTypes.StatementCoverage, BranchCoverage: extHostTypes.BranchCoverage, DeclarationCoverage: extHostTypes.DeclarationCoverage, - FunctionCoverage: extHostTypes.DeclarationCoverage, // back compat for Feb 2024 WorkspaceTrustState: extHostTypes.WorkspaceTrustState, LanguageStatusSeverity: extHostTypes.LanguageStatusSeverity, QuickPickItemKind: extHostTypes.QuickPickItemKind, @@ -1698,32 +1804,48 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, ChatCopyKind: extHostTypes.ChatCopyKind, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, - StackFrame: extHostTypes.StackFrame, - Thread: extHostTypes.Thread, + DebugStackFrame: extHostTypes.DebugStackFrame, + DebugThread: extHostTypes.DebugThread, RelatedInformationType: extHostTypes.RelatedInformationType, SpeechToTextStatus: extHostTypes.SpeechToTextStatus, + TextToSpeechStatus: extHostTypes.TextToSpeechStatus, PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, + ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, + ChatResponseReferencePart2: extHostTypes.ChatResponseReferencePart, + ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart, + ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, + ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, + ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, + ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart, + ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, + ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, - LanguageModelChatSystemMessage: extHostTypes.LanguageModelChatSystemMessage, - LanguageModelChatUserMessage: extHostTypes.LanguageModelChatUserMessage, - LanguageModelChatAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage, - LanguageModelSystemMessage: extHostTypes.LanguageModelChatSystemMessage, - LanguageModelUserMessage: extHostTypes.LanguageModelChatUserMessage, - LanguageModelAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage, + ChatRequestEditorData: extHostTypes.ChatRequestEditorData, + ChatRequestNotebookData: extHostTypes.ChatRequestNotebookData, + LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, + LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, + LanguageModelChatMessageFunctionResultPart: extHostTypes.LanguageModelFunctionResultPart, + LanguageModelChatResponseTextPart: extHostTypes.LanguageModelTextPart, + LanguageModelChatResponseFunctionUsePart: extHostTypes.LanguageModelFunctionUsePart, LanguageModelError: extHostTypes.LanguageModelError, NewSymbolName: extHostTypes.NewSymbolName, NewSymbolNameTag: extHostTypes.NewSymbolNameTag, + NewSymbolNameTriggerKind: extHostTypes.NewSymbolNameTriggerKind, InlineEdit: extHostTypes.InlineEdit, InlineEditTriggerKind: extHostTypes.InlineEditTriggerKind, + ExcludeSettingOptions: ExcludeSettingOptions, + TextSearchContextNew: TextSearchContextNew, + TextSearchMatchNew: TextSearchMatchNew, + TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageTypeNew, }; }; } diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index d01a3219f94..0427ebe7b17 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -31,6 +31,7 @@ import { ExtHostManagedSockets, IExtHostManagedSockets } from 'vs/workbench/api/ import { ExtHostAuthentication, IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostLanguageModels, IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; +import { ExtHostTesting, IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed); registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed); @@ -40,6 +41,7 @@ registerSingleton(IExtHostAuthentication, ExtHostAuthentication, InstantiationTy registerSingleton(IExtHostLanguageModels, ExtHostLanguageModels, InstantiationType.Eager); registerSingleton(IExtHostConfiguration, ExtHostConfiguration, InstantiationType.Eager); registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem, InstantiationType.Eager); +registerSingleton(IExtHostTesting, ExtHostTesting, InstantiationType.Eager); registerSingleton(IExtHostDebugService, WorkerExtHostDebugService, InstantiationType.Eager); registerSingleton(IExtHostDecorations, ExtHostDecorations, InstantiationType.Eager); registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors, InstantiationType.Eager); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c4f5c720677..ad8ed5c219e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -39,6 +39,7 @@ import { IMarkerData } from 'vs/platform/markers/common/markers'; import { IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress'; import * as quickInput from 'vs/platform/quickinput/common/quickInput'; import { IRemoteConnectionData, TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; @@ -50,13 +51,13 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; -import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatFollowup, IInlineChatProgressItem, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IToolData, IToolDelta, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -64,12 +65,12 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol, NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; -import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechToTextEvent } from 'vs/workbench/contrib/speech/common/speechService'; -import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechToTextEvent, ITextToSpeechEvent } from 'vs/workbench/contrib/speech/common/speechService'; +import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestControllerCapability, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; @@ -81,6 +82,7 @@ import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel'; import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder'; import * as search from 'vs/workbench/services/search/common/search'; import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import type { TerminalShellExecutionCommandLineConfidence } from 'vscode'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -143,10 +145,12 @@ export interface MainThreadCommentsShape extends IDisposable { $registerCommentController(handle: number, id: string, label: string, extensionId: string): void; $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; - $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, isTemplate: boolean): languages.CommentThread | undefined; + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, comments: languages.Comment[], extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string): languages.CommentThread | undefined; $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint): void; + $revealCommentThread(handle: number, commentThreadHandle: number, commentUniqueIdInThread: number, options: languages.CommentThreadRevealOptions): Promise; + $hideCommentThread(handle: number, commentThreadHandle: number): void; } export interface AuthenticationForceNewSessionOptions { @@ -160,7 +164,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; $getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; clearSessionPreference?: boolean }): Promise; - $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise; + $getAccounts(providerId: string): Promise>; $removeSession(providerId: string, sessionId: string): Promise; } @@ -262,7 +266,7 @@ export interface ITextDocumentShowOptions { } export interface MainThreadBulkEditsShape extends IDisposable { - $tryApplyWorkspaceEdit(workspaceEditDto: IWorkspaceEditDto, undoRedoGroupId?: number, respectAutoSaveConfig?: boolean): Promise; + $tryApplyWorkspaceEdit(workspaceEditDto: SerializableObjectWithBuffers, undoRedoGroupId?: number, respectAutoSaveConfig?: boolean): Promise; } export interface MainThreadTextEditorsShape extends IDisposable { @@ -414,7 +418,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerMultiDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; + $registerCodeActionSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, extensionID: string, supportsResolve: boolean): void; $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IPasteEditProviderMetadataDto): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string, supportRanges: boolean): void; @@ -442,7 +446,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void; } export interface MainThreadLanguagesShape extends IDisposable { @@ -769,7 +773,6 @@ export interface InteractiveEditorInputDto { export interface ChatEditorInputDto { kind: TabInputKind.ChatEditorInput; - providerId: string; } export interface MultiDiffEditorInputDto { @@ -1181,6 +1184,7 @@ export interface MainThreadSpeechShape extends IDisposable { $unregisterProvider(handle: number): void; $emitSpeechToTextEvent(session: number, event: ISpeechToTextEvent): void; + $emitTextToSpeechEvent(session: number, event: ITextToSpeechEvent): void; $emitKeywordRecognitionEvent(session: number, event: IKeywordRecognitionEvent): void; } @@ -1188,6 +1192,10 @@ export interface ExtHostSpeechShape { $createSpeechToTextSession(handle: number, session: number, language?: string): Promise; $cancelSpeechToTextSession(session: number): Promise; + $createTextToSpeechSession(handle: number, session: number, language?: string): Promise; + $synthesizeSpeech(session: number, text: string): Promise; + $cancelTextToSpeechSession(session: number): Promise; + $createKeywordRecognitionSession(handle: number, session: number): Promise; $cancelKeywordRecognitionSession(session: number): Promise; } @@ -1195,42 +1203,73 @@ export interface ExtHostSpeechShape { export interface MainThreadLanguageModelsShape extends IDisposable { $registerLanguageModelProvider(handle: number, identifier: string, metadata: ILanguageModelChatMetadata): void; $unregisterProvider(handle: number): void; - $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise; - - $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise; - $fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; + $tryStartChatRequest(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; + $reportResponsePart(requestId: number, chunk: IChatResponseFragment): Promise; + $reportResponseDone(requestId: number, error: SerializedError | undefined): Promise; + $selectChatModels(selector: ILanguageModelChatSelector): Promise; + $whenLanguageModelChatRequestMade(identifier: string, extension: ExtensionIdentifier, participant?: string, tokenCount?: number): void; + $countTokens(provider: string, value: string | IChatMessage, token: CancellationToken): Promise; } export interface ExtHostLanguageModelsShape { - $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[]; removed?: string[] }): void; + $acceptChatModelMetadata(data: ILanguageModelsChangeEvent): void; $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void; - $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; - $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise; + $startChatRequest(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; + $acceptResponsePart(requestId: number, chunk: IChatResponseFragment): Promise; + $acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise; + $provideTokenLength(handle: number, value: string | IChatMessage, token: CancellationToken): Promise; +} + +export interface MainThreadEmbeddingsShape extends IDisposable { + $registerEmbeddingProvider(handle: number, identifier: string): void; + $unregisterEmbeddingProvider(handle: number): void; + $computeEmbeddings(embeddingsModel: string, input: string[], token: CancellationToken): Promise<({ values: number[] }[])>; +} + +export interface ExtHostEmbeddingsShape { + $provideEmbeddings(handle: number, input: string[], token: CancellationToken): Promise<{ values: number[] }[]>; + $acceptEmbeddingModels(models: string[]): void; } export interface IExtensionChatAgentMetadata extends Dto { hasFollowups?: boolean; } +export interface IDynamicChatAgentProps { + name: string; + publisherName: string; + description?: string; + fullName?: string; +} + export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void; - $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; - $unregisterAgentCompletionsProvider(handle: number): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void; + $registerChatParticipantDetectionProvider(handle: number): void; + $unregisterChatParticipantDetectionProvider(handle: number): void; + $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; + $unregisterAgentCompletionsProvider(handle: number, id: string): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; $unregisterAgent(handle: number): void; - $handleProgressChunk(requestId: string, chunk: IChatProgressDto): Promise; + $handleProgressChunk(requestId: string, chunk: IChatProgressDto, handle?: number): Promise; + + $transferActiveChatSession(toWorkspace: UriComponents): void; } export interface IChatAgentCompletionItem { + id: string; + fullName?: string; + icon?: string; insertText?: string; label: string | languages.CompletionItemLabel; - values: IChatRequestVariableValueDto[]; + value: IChatRequestVariableValueDto; detail?: string; documentation?: string | IMarkdownString; + command?: ICommandDto; } export type IChatContentProgressDto = - | Dto; + | Dto> + | IChatTaskDto; export type IChatAgentHistoryEntryDto = { request: IChatAgentRequest; @@ -1239,14 +1278,25 @@ export type IChatAgentHistoryEntryDto = { }; export interface ExtHostChatAgentsShape2 { - $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; + $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; + $provideFollowups(request: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; + $acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; - $provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>; - $provideSampleQuestions(handle: number, token: CancellationToken): Promise; + $provideWelcomeMessage(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>; + $provideSampleQuestions(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise; $releaseSession(sessionId: string): void; + $detectChatParticipant(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise; +} +export interface IChatParticipantMetadata { + participant: string; + command?: string; + description?: string; +} + +export interface IChatParticipantDetectionResult { + participant: string; + command?: string; } export type IChatVariableResolverProgressDto = @@ -1258,26 +1308,22 @@ export interface MainThreadChatVariablesShape extends IDisposable { $unregisterVariable(handle: number): void; } +export interface MainThreadLanguageModelToolsShape extends IDisposable { + $getTools(): Promise[]>; + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise; + $registerTool(id: string): void; + $unregisterTool(name: string): void; +} + export type IChatRequestVariableValueDto = Dto; export interface ExtHostChatVariablesShape { - $resolveVariable(handle: number, requestId: string, messageText: string, token: CancellationToken): Promise; + $resolveVariable(handle: number, requestId: string, messageText: string, token: CancellationToken): Promise; } -export interface MainThreadInlineChatShape extends IDisposable { - $registerInteractiveEditorProvider(handle: number, label: string, extensionId: ExtensionIdentifier, supportsFeedback: boolean, supportsFollowups: boolean, supportsIssueReporting: boolean): Promise; - $handleProgressChunk(requestId: string, chunk: Dto): Promise; - $unregisterInteractiveEditorProvider(handle: number): Promise; -} - -export type IInlineChatResponseDto = Dto & { edits: IWorkspaceEditDto }>; - -export interface ExtHostInlineChatShape { - $prepareSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise; - $provideResponse(handle: number, session: IInlineChatSession, request: IInlineChatRequest, token: CancellationToken): Promise; - $provideFollowups(handle: number, sessionId: number, responseId: number, token: CancellationToken): Promise; - $handleFeedback(handle: number, sessionId: number, responseId: number, kind: InlineChatResponseFeedbackKind): void; - $releaseSession(handle: number, sessionId: number): void; +export interface ExtHostLanguageModelToolsShape { + $acceptToolDelta(delta: IToolDelta): Promise; + $invokeTool(id: string, parameters: any, token: CancellationToken): Promise; } export interface MainThreadUrlsShape extends IDisposable { @@ -1287,7 +1333,6 @@ export interface MainThreadUrlsShape extends IDisposable { } export interface IChatDto { - id: number; } export interface IChatRequestDto { @@ -1316,19 +1361,8 @@ export type IDocumentContextDto = { }; export type IChatProgressDto = - | Dto; - -export interface MainThreadChatShape extends IDisposable { - $registerChatProvider(handle: number, id: string): Promise; - $acceptChatState(sessionId: number, state: any): Promise; - $unregisterChatProvider(handle: number): Promise; - $transferChatSession(sessionId: number, toWorkspace: UriComponents): void; -} - -export interface ExtHostChatShape { - $prepareChat(handle: number, token: CancellationToken): Promise; - $releaseSession(sessionId: number): void; -} + | Dto> + | IChatTaskDto; export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; @@ -1366,6 +1400,8 @@ export interface MainThreadWorkspaceShape extends IDisposable { $saveAll(includeUntitled?: boolean): Promise; $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents; name?: string }[]): Promise; $resolveProxy(url: string): Promise; + $lookupAuthorization(authInfo: AuthInfo): Promise; + $lookupKerberosAuthorization(url: string): Promise; $loadCertificates(): Promise; $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; $registerEditSessionIdentityProvider(handle: number, scheme: string): void; @@ -1493,17 +1529,28 @@ export type SCMRawResourceSplices = [ export interface SCMHistoryItemGroupDto { readonly id: string; - readonly label: string; - readonly base?: Omit; + readonly name: string; + readonly revision?: string; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; } export interface SCMHistoryItemDto { readonly id: string; readonly parentIds: string[]; - readonly label: string; - readonly description?: string; + readonly message: string; + readonly author?: string; readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; readonly timestamp?: number; + readonly statistics?: { + readonly files: number; + readonly insertions: number; + readonly deletions: number; + }; + readonly labels?: { + readonly title: string; + readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; + }[]; } export interface SCMHistoryItemChangeDto { @@ -1514,25 +1561,25 @@ export interface SCMHistoryItemChangeDto { } export interface MainThreadSCMShape extends IDisposable { - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void; - $updateSourceControl(handle: number, features: SCMProviderFeatures): void; - $unregisterSourceControl(handle: number): void; + $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise; + $updateSourceControl(handle: number, features: SCMProviderFeatures): Promise; + $unregisterSourceControl(handle: number): Promise; - $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): void; - $updateGroup(sourceControlHandle: number, handle: number, features: SCMGroupFeatures): void; - $updateGroupLabel(sourceControlHandle: number, handle: number, label: string): void; - $unregisterGroup(sourceControlHandle: number, handle: number): void; + $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): Promise; + $updateGroup(sourceControlHandle: number, handle: number, features: SCMGroupFeatures): Promise; + $updateGroupLabel(sourceControlHandle: number, handle: number, label: string): Promise; + $unregisterGroup(sourceControlHandle: number, handle: number): Promise; - $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): void; + $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): Promise; - $setInputBoxValue(sourceControlHandle: number, value: string): void; - $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void; - $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void; - $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void; - $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void; - $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void; + $setInputBoxValue(sourceControlHandle: number, value: string): Promise; + $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): Promise; + $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): Promise; + $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): Promise; + $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise; + $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise; - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void; + $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise; } export interface MainThreadQuickDiffShape extends IDisposable { @@ -1559,6 +1606,7 @@ export interface IStartDebuggingOptions { suppressDebugStatusbar?: boolean; suppressDebugView?: boolean; suppressSaveBeforeStart?: boolean; + testRun?: IDebugTestRunReference; } export interface MainThreadDebugServiceShape extends IDisposable { @@ -1599,7 +1647,8 @@ export interface MainThreadWindowShape extends IDisposable { export enum CandidatePortSource { None = 0, Process = 1, - Output = 2 + Output = 2, + Hybrid = 3 } export interface PortAttributesSelector { @@ -1626,6 +1675,13 @@ export interface MainThreadTimelineShape extends IDisposable { $emitTimelineChangeEvent(e: TimelineChangeEvent | undefined): void; } +export interface HoverWithId extends languages.Hover { + /** + * Id of the hover + */ + id: number; +} + // -- extension host export interface ICommandMetadataDto { @@ -1790,7 +1846,7 @@ export interface ExtHostLabelServiceShape { } export interface ExtHostAuthenticationShape { - $getSessions(id: string, scopes?: string[]): Promise>; + $getSessions(id: string, scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise>; $createSession(id: string, scopes: string[], options: IAuthenticationCreateSessionOptions): Promise; $removeSession(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string): Promise; @@ -2082,7 +2138,6 @@ export interface IPasteEditProviderMetadataDto { export interface IDocumentPasteContextDto { readonly only: string | undefined; readonly triggerKind: languages.DocumentPasteTriggerKind; - } export interface IPasteEditDto { @@ -2095,10 +2150,13 @@ export interface IPasteEditDto { } export interface IDocumentDropEditProviderMetadata { + readonly supportsResolve: boolean; + dropMimeTypes: readonly string[]; } -export interface IDocumentOnDropEditDto { +export interface IDocumentDropEditDto { + _cacheId?: ChainedCacheId; title: string; kind: string | undefined; insertText: string | { snippet: string }; @@ -2115,7 +2173,8 @@ export interface ExtHostLanguageFeaturesShape { $provideDeclaration(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideImplementation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; - $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $provideHover(handle: number, resource: UriComponents, position: IPosition, context: languages.HoverContext<{ id: number }> | undefined, token: CancellationToken): Promise; + $releaseHover(handle: number, id: number): void; $provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideInlineValues(handle: number, resource: UriComponents, range: IRange, context: languages.InlineValueContext, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; @@ -2138,7 +2197,8 @@ export interface ExtHostLanguageFeaturesShape { $releaseWorkspaceSymbols(handle: number, id: number): void; $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Promise; $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; - $provideNewSymbolNames(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise; + $supportsAutomaticNewSymbolNamesTriggerKind(handle: number): Promise; + $provideNewSymbolNames(handle: number, resource: UriComponents, range: IRange, triggerKind: languages.NewSymbolNameTriggerKind, token: CancellationToken): Promise; $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise; $releaseDocumentSemanticTokens(handle: number, semanticColoringResultId: number): void; $provideDocumentRangeSemanticTokens(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise; @@ -2146,6 +2206,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; + $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; @@ -2170,7 +2231,8 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySupertypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseTypeHierarchy(handle: number, sessionId: string): void; - $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; + $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void; $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; @@ -2268,10 +2330,10 @@ export interface ExtHostTerminalServiceShape { export interface ExtHostTerminalShellIntegrationShape { $shellIntegrationChange(instanceId: number): void; - $shellExecutionStart(instanceId: number, commandLine: string | undefined, cwd: UriComponents | string | undefined): void; - $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void; + $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void; + $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void; $shellExecutionData(instanceId: number, data: string): void; - $cwdChange(instanceId: number, cwd: UriComponents | string): void; + $cwdChange(instanceId: number, cwd: UriComponents | undefined): void; $closeTerminal(instanceId: number): void; } @@ -2282,9 +2344,11 @@ export interface ExtHostSCMShape { $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise; $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise; + $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; + $resolveHistoryItemGroupCommonAncestor2(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise; } export interface ExtHostQuickDiffShape { @@ -2453,7 +2517,7 @@ export interface ExtHostProgressShape { } export interface ExtHostCommentsShape { - $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined): Promise; + $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined, editorId?: string): Promise; $updateCommentThreadTemplate(commentControllerHandle: number, threadHandle: number, range: IRange): Promise; $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number): void; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<{ ranges: IRange[]; fileComments: boolean } | undefined>; @@ -2650,19 +2714,6 @@ export interface MainThreadLocalizationShape extends IDisposable { $fetchBundleContents(uriComponents: UriComponents): Promise; } -export interface ExtHostIssueReporterShape { - $getIssueReporterUri(extensionId: string, token: CancellationToken): Promise; - $getIssueReporterData(extensionId: string, token: CancellationToken): Promise; - $getIssueReporterTemplate(extensionId: string, token: CancellationToken): Promise; -} - -export interface MainThreadIssueReporterShape extends IDisposable { - $registerIssueUriRequestHandler(extensionId: string): void; - $unregisterIssueUriRequestHandler(extensionId: string): void; - $registerIssueDataProvider(extensionId: string): void; - $unregisterIssueDataProvider(extensionId: string): void; -} - export interface TunnelDto { remoteAddress: { port: number; host: string }; localAddress: { port: number; host: string } | string; @@ -2693,15 +2744,13 @@ export const enum ExtHostTestingResource { export interface ExtHostTestingShape { $runControllerTests(req: IStartControllerTests[], token: CancellationToken): Promise<{ error?: string }[]>; $startContinuousRun(req: ICallProfileRunHandler[], token: CancellationToken): Promise<{ error?: string }[]>; - $cancelExtensionTestRun(runId: string | undefined): void; + $cancelExtensionTestRun(runId: string | undefined, taskId: string | undefined): void; /** Handles a diff of tests, as a result of a subscribeToDiffs() call */ $acceptDiff(diff: TestsDiffOp.Serialized[]): void; - /** Publishes that a test run finished. */ - $publishTestResults(results: ISerializedTestResults[]): void; /** Expands a test item's children, by the given number of levels. */ $expandTest(testId: string, levels: number): Promise; /** Requests coverage details for a test run. Errors if not available. */ - $getCoverageDetails(coverageId: string, token: CancellationToken): Promise; + $getCoverageDetails(coverageId: string, testId: string | undefined, token: CancellationToken): Promise; /** Disposes resources associated with a test run. */ $disposeRun(runId: string): void; /** Configures a test run config. */ @@ -2712,6 +2761,19 @@ export interface ExtHostTestingShape { $syncTests(): Promise; /** Sets the active test run profiles */ $setDefaultRunProfiles(profiles: Record): void; + $getTestsRelatedToCode(uri: UriComponents, position: IPosition, token: CancellationToken): Promise; + $getCodeRelatedToTest(testId: string, token: CancellationToken): Promise; + + // --- test results: + + /** Publishes that a test run finished. */ + $publishTestResults(results: ISerializedTestResults[]): void; + /** Requests followup actions for a test (failure) message */ + $provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; + /** Actions a followup actions for a test (failure) message */ + $executeTestFollowup(id: number): Promise; + /** Disposes followup actions for a test (failure) message */ + $disposeTestFollowups(id: number[]): void; } export interface ExtHostLocalizationShape { @@ -2729,14 +2791,14 @@ export interface IStringDetails { export interface ITestControllerPatch { label?: string; - canRefresh?: boolean; + capabilities?: TestControllerCapability; } export interface MainThreadTestingShape { // --- test lifecycle: /** Registers that there's a test controller with the given ID */ - $registerTestController(controllerId: string, label: string, canRefresh: boolean): void; + $registerTestController(controllerId: string, label: string, capability: TestControllerCapability): void; /** Updates the label of an existing test controller. */ $updateController(controllerId: string, patch: ITestControllerPatch): void; /** Diposes of the test controller with the given ID */ @@ -2793,8 +2855,10 @@ export const MainContext = { MainThreadAuthentication: createProxyIdentifier('MainThreadAuthentication'), MainThreadBulkEdits: createProxyIdentifier('MainThreadBulkEdits'), MainThreadLanguageModels: createProxyIdentifier('MainThreadLanguageModels'), + MainThreadEmbeddings: createProxyIdentifier('MainThreadEmbeddings'), MainThreadChatAgents2: createProxyIdentifier('MainThreadChatAgents2'), MainThreadChatVariables: createProxyIdentifier('MainThreadChatVariables'), + MainThreadLanguageModelTools: createProxyIdentifier('MainThreadChatSkills'), MainThreadClipboard: createProxyIdentifier('MainThreadClipboard'), MainThreadCommands: createProxyIdentifier('MainThreadCommands'), MainThreadComments: createProxyIdentifier('MainThreadComments'), @@ -2850,8 +2914,6 @@ export const MainContext = { MainThreadNotebookKernels: createProxyIdentifier('MainThreadNotebookKernels'), MainThreadNotebookRenderers: createProxyIdentifier('MainThreadNotebookRenderers'), MainThreadInteractive: createProxyIdentifier('MainThreadInteractive'), - MainThreadChat: createProxyIdentifier('MainThreadChat'), - MainThreadInlineChat: createProxyIdentifier('MainThreadInlineChatShape'), MainThreadTheming: createProxyIdentifier('MainThreadTheming'), MainThreadTunnelService: createProxyIdentifier('MainThreadTunnelService'), MainThreadManagedSockets: createProxyIdentifier('MainThreadManagedSockets'), @@ -2859,8 +2921,7 @@ export const MainContext = { MainThreadTesting: createProxyIdentifier('MainThreadTesting'), MainThreadLocalization: createProxyIdentifier('MainThreadLocalizationShape'), MainThreadAiRelatedInformation: createProxyIdentifier('MainThreadAiRelatedInformation'), - MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), - MainThreadIssueReporter: createProxyIdentifier('MainThreadIssueReporter'), + MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector') }; export const ExtHostContext = { @@ -2915,12 +2976,12 @@ export const ExtHostContext = { ExtHostNotebookRenderers: createProxyIdentifier('ExtHostNotebookRenderers'), ExtHostNotebookDocumentSaveParticipant: createProxyIdentifier('ExtHostNotebookDocumentSaveParticipant'), ExtHostInteractive: createProxyIdentifier('ExtHostInteractive'), - ExtHostInlineChat: createProxyIdentifier('ExtHostInlineChatShape'), - ExtHostChat: createProxyIdentifier('ExtHostChat'), ExtHostChatAgents2: createProxyIdentifier('ExtHostChatAgents'), ExtHostChatVariables: createProxyIdentifier('ExtHostChatVariables'), + ExtHostLanguageModelTools: createProxyIdentifier('ExtHostChatSkills'), ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), ExtHostSpeech: createProxyIdentifier('ExtHostSpeech'), + ExtHostEmbeddings: createProxyIdentifier('ExtHostEmbeddings'), ExtHostAiRelatedInformation: createProxyIdentifier('ExtHostAiRelatedInformation'), ExtHostAiEmbeddingVector: createProxyIdentifier('ExtHostAiEmbeddingVector'), ExtHostTheming: createProxyIdentifier('ExtHostTheming'), @@ -2930,6 +2991,5 @@ export const ExtHostContext = { ExtHostTimeline: createProxyIdentifier('ExtHostTimeline'), ExtHostTesting: createProxyIdentifier('ExtHostTesting'), ExtHostTelemetry: createProxyIdentifier('ExtHostTelemetry'), - ExtHostLocalization: createProxyIdentifier('ExtHostLocalization'), - ExtHostIssueReporter: createProxyIdentifier('ExtHostIssueReporter'), + ExtHostLocalization: createProxyIdentifier('ExtHostLocalization') }; diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 62f9ebbf690..6384178b82e 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -9,6 +9,7 @@ import { Schemas, matchesSomeScheme } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; +import { ISelection } from 'vs/editor/common/core/selection'; import * as languages from 'vs/editor/common/languages'; import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { validateWhenClauses } from 'vs/platform/contextkey/common/contextkey'; @@ -87,32 +88,62 @@ const newCommands: ApiCommand[] = [ [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeDefinitionProvider_recursive', '_executeDefinitionProvider_recursive', 'Execute all definition providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeTypeDefinitionProvider', '_executeTypeDefinitionProvider', 'Execute all type definition providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeTypeDefinitionProvider_recursive', '_executeTypeDefinitionProvider_recursive', 'Execute all type definition providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeDeclarationProvider', '_executeDeclarationProvider', 'Execute all declaration providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeDeclarationProvider_recursive', '_executeDeclarationProvider_recursive', 'Execute all declaration providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeImplementationProvider', '_executeImplementationProvider', 'Execute all implementation providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), + new ApiCommand( + 'vscode.experimental.executeImplementationProvider_recursive', '_executeImplementationProvider_recursive', 'Execute all implementation providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult<(languages.Location | languages.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) + ), new ApiCommand( 'vscode.executeReferenceProvider', '_executeReferenceProvider', 'Execute all reference providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A promise that resolves to an array of Location-instances.', tryMapWith(typeConverters.location.to)) ), + new ApiCommand( + 'vscode.experimental.executeReferenceProvider', '_executeReferenceProvider_recursive', 'Execute all reference providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult('A promise that resolves to an array of Location-instances.', tryMapWith(typeConverters.location.to)) + ), // -- hover new ApiCommand( 'vscode.executeHoverProvider', '_executeHoverProvider', 'Execute all hover providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], new ApiCommandResult('A promise that resolves to an array of Hover-instances.', tryMapWith(typeConverters.Hover.to)) ), + new ApiCommand( + 'vscode.experimental.executeHoverProvider_recursive', '_executeHoverProvider_recursive', 'Execute all hover providers.', + [ApiCommandArgument.Uri, ApiCommandArgument.Position], + new ApiCommandResult('A promise that resolves to an array of Hover-instances.', tryMapWith(typeConverters.Hover.to)) + ), // -- selection range new ApiCommand( 'vscode.executeSelectionRangeProvider', '_executeSelectionRangeProvider', 'Execute selection range provider.', @@ -512,8 +543,43 @@ const newCommands: ApiCommand[] = [ return value ? typeConverters.WorkspaceEdit.to(value) : null; }) ), + // --- inline chat + new ApiCommand( + 'vscode.editorChat.start', 'inlineChat.start', 'Invoke a new editor chat session', + [new ApiCommandArgument('Run arguments', '', _v => true, v => { + + if (!v) { + return undefined; + } + + return { + initialRange: v.initialRange ? typeConverters.Range.from(v.initialRange) : undefined, + initialSelection: types.Selection.isSelection(v.initialSelection) ? typeConverters.Selection.from(v.initialSelection) : undefined, + message: v.message, + autoSend: v.autoSend, + position: v.position ? typeConverters.Position.from(v.position) : undefined, + }; + })], + ApiCommandResult.Void + ) ]; +type InlineChatEditorApiArg = { + initialRange?: vscode.Range; + initialSelection?: vscode.Selection; + message?: string; + autoSend?: boolean; + position?: vscode.Position; +}; + +type InlineChatRunOptions = { + initialRange?: IRange; + initialSelection?: ISelection; + message?: string; + autoSend?: boolean; + position?: IPosition; +}; + //#endregion diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 1c562edf76a..c5ba402ef07 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -32,7 +32,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; private _getSessionTaskSingler = new TaskSingler(); - private _getSessionsTaskSingler = new TaskSingler>(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService @@ -54,14 +53,9 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } - async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[]): Promise> { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - const sortedScopes = [...scopes].sort().join(' '); - return await this._getSessionsTaskSingler.getOrCreate(`${extensionId} ${sortedScopes}`, async () => { - await this._proxy.$ensureProvider(providerId); - const extensionName = requestingExtension.displayName || requestingExtension.name; - return this._proxy.$getSessions(providerId, scopes, extensionId, extensionName); - }); + async getAccounts(providerId: string) { + await this._proxy.$ensureProvider(providerId); + return await this._proxy.$getAccounts(providerId); } async removeSession(providerId: string, sessionId: string): Promise { @@ -89,28 +83,28 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } - $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderCreateSessionOptions): Promise { + async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderSessionOptions): Promise { const providerData = this._authenticationProviders.get(providerId); if (providerData) { - return Promise.resolve(providerData.provider.createSession(scopes, options)); + return await providerData.provider.createSession(scopes, options); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - $removeSession(providerId: string, sessionId: string): Promise { + async $removeSession(providerId: string, sessionId: string): Promise { const providerData = this._authenticationProviders.get(providerId); if (providerData) { - return Promise.resolve(providerData.provider.removeSession(sessionId)); + return await providerData.provider.removeSession(sessionId); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - $getSessions(providerId: string, scopes?: string[]): Promise> { + async $getSessions(providerId: string, scopes: ReadonlyArray | undefined, options: vscode.AuthenticationProviderSessionOptions): Promise> { const providerData = this._authenticationProviders.get(providerId); if (providerData) { - return Promise.resolve(providerData.provider.getSessions(scopes)); + return await providerData.provider.getSessions(scopes, options); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); diff --git a/src/vs/workbench/api/common/extHostBulkEdits.ts b/src/vs/workbench/api/common/extHostBulkEdits.ts index bdbfe2e5ba8..281a003c40c 100644 --- a/src/vs/workbench/api/common/extHostBulkEdits.ts +++ b/src/vs/workbench/api/common/extHostBulkEdits.ts @@ -8,6 +8,7 @@ import { MainContext, MainThreadBulkEditsShape } from 'vs/workbench/api/common/e import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters'; +import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; export class ExtHostBulkEdits { @@ -28,7 +29,7 @@ export class ExtHostBulkEdits { } applyWorkspaceEdit(edit: vscode.WorkspaceEdit, extension: IExtensionDescription, metadata: vscode.WorkspaceEditMetadata | undefined): Promise { - const dto = WorkspaceEdit.from(edit, this._versionInformationProvider); + const dto = new SerializableObjectWithBuffers(WorkspaceEdit.from(edit, this._versionInformationProvider)); return this._proxy.$tryApplyWorkspaceEdit(dto, undefined, metadata?.isRefactoring ?? false); } } diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts deleted file mode 100644 index 036d64b14d2..00000000000 --- a/src/vs/workbench/api/common/extHostChat.ts +++ /dev/null @@ -1,85 +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 'vs/base/common/cancellation'; -import { Iterable } from 'vs/base/common/iterator'; -import { toDisposable } from 'vs/base/common/lifecycle'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtHostChatShape, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; -import type * as vscode from 'vscode'; - -class ChatProviderWrapper { - - private static _pool = 0; - - readonly handle: number = ChatProviderWrapper._pool++; - - constructor( - readonly extension: Readonly, - readonly provider: T, - ) { } -} - -export class ExtHostChat implements ExtHostChatShape { - private static _nextId = 0; - - private readonly _chatProvider = new Map>(); - - private readonly _chatSessions = new Map(); - - private readonly _proxy: MainThreadChatShape; - - constructor( - mainContext: IMainContext, - ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadChat); - } - - //#region interactive session - - registerChatProvider(extension: Readonly, id: string, provider: vscode.InteractiveSessionProvider): vscode.Disposable { - const wrapper = new ChatProviderWrapper(extension, provider); - this._chatProvider.set(wrapper.handle, wrapper); - this._proxy.$registerChatProvider(wrapper.handle, id); - return toDisposable(() => { - this._proxy.$unregisterChatProvider(wrapper.handle); - this._chatProvider.delete(wrapper.handle); - }); - } - - transferChatSession(session: vscode.InteractiveSession, newWorkspace: vscode.Uri): void { - const sessionId = Iterable.find(this._chatSessions.keys(), key => this._chatSessions.get(key) === session) ?? 0; - if (typeof sessionId !== 'number') { - return; - } - - this._proxy.$transferChatSession(sessionId, newWorkspace); - } - - async $prepareChat(handle: number, token: CancellationToken): Promise { - const entry = this._chatProvider.get(handle); - if (!entry) { - return undefined; - } - - const session = await entry.provider.prepareSession(token); - if (!session) { - return undefined; - } - - const id = ExtHostChat._nextId++; - this._chatSessions.set(id, session); - - return { - id, - }; - } - - $releaseSession(sessionId: number) { - this._chatSessions.delete(sessionId); - } - - //#endregion -} diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 80a4beaffaf..b366aa2a986 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Location } from 'vs/editor/common/languages'; import { coalesce } from 'vs/base/common/arrays'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -11,19 +10,21 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; -import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { localize } from 'vs/nls'; +import { Location } from 'vs/editor/common/languages'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IMainContext, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatProgressDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContentReference, IChatFollowup, IChatProgress, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; @@ -33,13 +34,12 @@ class ChatAgentResponseStream { private _stopWatch = StopWatch.create(false); private _isClosed: boolean = false; private _firstProgress: number | undefined; - private _apiObject: vscode.ChatExtendedResponseStream | undefined; + private _apiObject: vscode.ChatResponseStream | undefined; constructor( private readonly _extension: IExtensionDescription, private readonly _request: IChatAgentRequest, private readonly _proxy: MainThreadChatAgentsShape2, - private readonly _logService: ILogService, private readonly _commandsConverter: CommandsConverter, private readonly _sessionDisposables: DisposableStore ) { } @@ -70,53 +70,103 @@ class ChatAgentResponseStream { } } - const _report = (progress: Dto) => { + const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - this._proxy.$handleProgressChunk(this._request.requestId, progress); + + if (task) { + const progressReporterPromise = this._proxy.$handleProgressChunk(this._request.requestId, progress); + const progressReporter = { + report: (p: vscode.ChatResponseWarningPart | vscode.ChatResponseReferencePart) => { + progressReporterPromise?.then((handle) => { + if (handle) { + if (extHostTypes.MarkdownString.isMarkdownString(p.value)) { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseWarningPart.from(p), handle); + } else { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseReferencePart.from(p), handle); + } + } + }); + } + }; + + Promise.all([progressReporterPromise, task?.(progressReporter)]).then(([handle, res]) => { + if (handle !== undefined) { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); + } + }); + } else { + this._proxy.$handleProgressChunk(this._request.requestId, progress); + } }; this._apiObject = { markdown(value) { throwIfDone(this.markdown); const part = new extHostTypes.ChatResponseMarkdownPart(value); - const dto = typeConvert.ChatResponseMarkdownPart.to(part); + const dto = typeConvert.ChatResponseMarkdownPart.from(part); + _report(dto); + return this; + }, + markdownWithVulnerabilities(value, vulnerabilities) { + throwIfDone(this.markdown); + if (vulnerabilities) { + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + } + + const part = new extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart(value, vulnerabilities); + const dto = typeConvert.ChatResponseMarkdownWithVulnerabilitiesPart.from(part); _report(dto); return this; }, filetree(value, baseUri) { throwIfDone(this.filetree); const part = new extHostTypes.ChatResponseFileTreePart(value, baseUri); - const dto = typeConvert.ChatResponseFilesPart.to(part); + const dto = typeConvert.ChatResponseFilesPart.from(part); _report(dto); return this; }, anchor(value, title?: string) { throwIfDone(this.anchor); const part = new extHostTypes.ChatResponseAnchorPart(value, title); - const dto = typeConvert.ChatResponseAnchorPart.to(part); + const dto = typeConvert.ChatResponseAnchorPart.from(part); _report(dto); return this; }, button(value) { throwIfDone(this.anchor); const part = new extHostTypes.ChatResponseCommandButtonPart(value); - const dto = typeConvert.ChatResponseCommandButtonPart.to(part, that._commandsConverter, that._sessionDisposables); + const dto = typeConvert.ChatResponseCommandButtonPart.from(part, that._commandsConverter, that._sessionDisposables); _report(dto); return this; }, - progress(value) { + progress(value, task?: ((progress: vscode.Progress) => Thenable)) { throwIfDone(this.progress); - const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.to(part); + const part = new extHostTypes.ChatResponseProgressPart2(value, task); + const dto = task ? typeConvert.ChatTask.from(part) : typeConvert.ChatResponseProgressPart.from(part); + _report(dto, task); + return this; + }, + warning(value) { + throwIfDone(this.progress); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + const part = new extHostTypes.ChatResponseWarningPart(value); + const dto = typeConvert.ChatResponseWarningPart.from(part); _report(dto); return this; }, - reference(value) { + reference(value, iconPath) { + return this.reference2(value, iconPath); + }, + reference2(value, iconPath, options) { throwIfDone(this.reference); + if ('variableName' in value) { + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + } + if ('variableName' in value && !value.value) { // The participant used this variable. Does that variable have any references to pull in? const matchingVarData = that._request.variables.variables.find(v => v.name === value.variableName); @@ -129,8 +179,8 @@ class ChatAgentResponseStream { } satisfies IChatContentReference)); } else { // Participant sent a variableName reference but the variable produced no references. Show variable reference with no value - const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.ChatResponseReferencePart.to(part); + const part = new extHostTypes.ChatResponseReferencePart(value, iconPath, options); + const dto = typeConvert.ChatResponseReferencePart.from(part); references = [dto]; } @@ -140,42 +190,72 @@ class ChatAgentResponseStream { // Something went wrong- that variable doesn't actually exist } } else { - const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.ChatResponseReferencePart.to(part); + const part = new extHostTypes.ChatResponseReferencePart(value, iconPath, options); + const dto = typeConvert.ChatResponseReferencePart.from(part); _report(dto); } return this; }, + codeCitation(value: vscode.Uri, license: string, snippet: string): void { + throwIfDone(this.codeCitation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseCodeCitationPart(value, license, snippet); + const dto = typeConvert.ChatResponseCodeCitationPart.from(part); + _report(dto); + }, + textEdit(target, edits) { + throwIfDone(this.textEdit); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseTextEditPart(target, edits); + const dto = typeConvert.ChatResponseTextEditPart.from(part); + _report(dto); + return this; + }, + detectedParticipant(participant, command) { + throwIfDone(this.detectedParticipant); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseDetectedParticipantPart(participant, command); + const dto = typeConvert.ChatResponseDetectedParticipantPart.from(part); + _report(dto); + return this; + }, + confirmation(title, message, data, buttons) { + throwIfDone(this.confirmation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data, buttons); + const dto = typeConvert.ChatResponseConfirmationPart.from(part); + _report(dto); + return this; + }, push(part) { throwIfDone(this.push); + if ( + part instanceof extHostTypes.ChatResponseTextEditPart || + part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || + part instanceof extHostTypes.ChatResponseDetectedParticipantPart || + part instanceof extHostTypes.ChatResponseWarningPart || + part instanceof extHostTypes.ChatResponseConfirmationPart || + part instanceof extHostTypes.ChatResponseCodeCitationPart + ) { + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + } + if (part instanceof extHostTypes.ChatResponseReferencePart) { // Ensure variable reference values get fixed up - this.reference(part.value); + this.reference2(part.value, part.iconPath, part.options); } else { - const dto = typeConvert.ChatResponsePart.to(part, that._commandsConverter, that._sessionDisposables); + const dto = typeConvert.ChatResponsePart.from(part, that._commandsConverter, that._sessionDisposables); _report(dto); } return this; }, - report(progress) { - throwIfDone(this.report); - if ('placeholder' in progress && 'resolvedContent' in progress) { - // Ignore for now, this is the deleted Task type - return; - } - - const value = typeConvert.ChatResponseProgress.from(that._extension, progress); - if (!value) { - that._logService.error('Unknown progress type: ' + JSON.stringify(progress)); - return; - } - - _report(value); - return this; - } }; } @@ -183,23 +263,33 @@ class ChatAgentResponseStream { } } -export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { +export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsShape2 { private static _idPool = 0; private readonly _agents = new Map(); private readonly _proxy: MainThreadChatAgentsShape2; - private readonly _sessionDisposables: DisposableMap = new DisposableMap(); + private static _participantDetectionProviderIdPool = 0; + private readonly _participantDetectionProviders = new Map(); + + private readonly _sessionDisposables: DisposableMap = this._register(new DisposableMap()); + private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); constructor( mainContext: IMainContext, private readonly _logService: ILogService, - private readonly commands: ExtHostCommands, + private readonly _commands: ExtHostCommands, + private readonly _documents: ExtHostDocuments ) { + super(); this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); } + transferActiveChat(newWorkspace: vscode.Uri): void { + this._proxy.$transferActiveChatSession(newWorkspace); + } + createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); @@ -209,34 +299,87 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { return agent.apiAgent; } - createDynamicChatAgent(extension: IExtensionDescription, id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + createDynamicChatAgent(extension: IExtensionDescription, id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, id, {}, { name, description }); + this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionChatAgentMetadata, dynamicProps); return agent.apiAgent; } - async $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { + registerChatParticipantDetectionProvider(provider: vscode.ChatParticipantDetectionProvider): vscode.Disposable { + const handle = ExtHostChatAgents2._participantDetectionProviderIdPool++; + this._participantDetectionProviders.set(handle, provider); + this._proxy.$registerChatParticipantDetectionProvider(handle); + return toDisposable(() => { + this._participantDetectionProviders.delete(handle); + this._proxy.$unregisterChatParticipantDetectionProvider(handle); + }); + } + + async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { + const { request, location, history } = await this._createRequest(requestDto, context); + + const provider = this._participantDetectionProviders.get(handle); + if (!provider) { + return undefined; + } + + return provider.provideParticipantDetection( + typeConvert.ChatAgentRequest.to(request, location), + { history }, + { participants: options.participants, location: typeConvert.ChatLocation.to(options.location) }, + token + ); + } + + private async _createRequest(requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }) { + const request = revive(requestDto); + const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); + + // in-place converting for location-data + let location: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined; + if (request.locationData?.type === ChatAgentLocation.Editor) { + // editor data + const document = this._documents.getDocument(request.locationData.document); + location = new extHostTypes.ChatRequestEditorData(document, typeConvert.Selection.to(request.locationData.selection), typeConvert.Range.to(request.locationData.wholeRange)); + + } else if (request.locationData?.type === ChatAgentLocation.Notebook) { + // notebook data + const cell = this._documents.getDocument(request.locationData.sessionInputUri); + location = new extHostTypes.ChatRequestNotebookData(cell); + + } else if (request.locationData?.type === ChatAgentLocation.Terminal) { + // TBD + } + + return { request, location, history: convertedHistory }; + } + + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); } - // Init session disposables - let sessionDisposables = this._sessionDisposables.get(request.sessionId); - if (!sessionDisposables) { - sessionDisposables = new DisposableStore(); - this._sessionDisposables.set(request.sessionId, sessionDisposables); - } + let stream: ChatAgentResponseStream | undefined; - const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._logService, this.commands.converter, sessionDisposables); try { - const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); + const { request, location, history } = await this._createRequest(requestDto, context); + + // Init session disposables + let sessionDisposables = this._sessionDisposables.get(request.sessionId); + if (!sessionDisposables) { + sessionDisposables = new DisposableStore(); + this._sessionDisposables.set(request.sessionId, sessionDisposables); + } + + stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); + const task = agent.invoke( - typeConvert.ChatAgentRequest.to(request), - { history: convertedHistory }, + typeConvert.ChatAgentRequest.to(request, location), + { history }, stream.apiObject, token ); @@ -248,18 +391,33 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { } catch (err) { const msg = `result.metadata MUST be JSON.stringify-able. Got error: ${err.message}`; this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] ${msg}`, agent.extension); - return { errorDetails: { message: msg }, timings: stream.timings }; + return { errorDetails: { message: msg }, timings: stream?.timings, nextQuestion: result.nextQuestion }; } } - return { errorDetails: result?.errorDetails, timings: stream.timings, metadata: result?.metadata }; - }), token); + let errorDetails: IChatResponseErrorDetails | undefined; + if (result?.errorDetails) { + errorDetails = { + ...result.errorDetails, + responseIsIncomplete: true + }; + } + if (errorDetails?.responseIsRedacted) { + checkProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); + } + return { errorDetails, timings: stream?.timings, metadata: result?.metadata, nextQuestion: result?.nextQuestion } satisfies IChatAgentResult; + }), token); } catch (e) { this._logService.error(e, agent.extension); - return { errorDetails: { message: localize('errorResponse', "Error from participant: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; + + if (e instanceof extHostTypes.LanguageModelError && e.cause) { + e = e.cause; + } + + return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true } }; } finally { - stream.close(); + stream?.close(); } } @@ -274,10 +432,13 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { { ...ehResult, metadata: undefined }; // REQUEST turn - res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), h.request.agentId)); + const varsWithoutTools = h.request.variables.variables + .filter(v => !v.isTool) + .map(typeConvert.ChatAgentValueReference.to); + res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId)); // RESPONSE turn - const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.fromContent(r, this.commands.converter))); + const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.toContent(r, this._commands.converter))); res.push(new extHostTypes.ChatResponseTurn(parts, result, h.request.agentId, h.request.command)); } @@ -288,12 +449,13 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { + async $provideFollowups(requestDto: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { return Promise.resolve([]); } + const request = revive(requestDto); const convertedHistory = await this.prepareHistoryTurns(agent.id, context); const ehResult = typeConvert.ChatAgentResult.to(result); @@ -311,7 +473,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { .map(f => typeConvert.ChatFollowup.from(f, request)); } - $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void { + $acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void { const agent = this._agents.get(handle); if (!agent) { return; @@ -320,10 +482,10 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { const ehResult = typeConvert.ChatAgentResult.to(result); let kind: extHostTypes.ChatResultFeedbackKind; switch (vote) { - case InteractiveSessionVoteDirection.Down: + case ChatAgentVoteDirection.Down: kind = extHostTypes.ChatResultFeedbackKind.Unhelpful; break; - case InteractiveSessionVoteDirection.Up: + case ChatAgentVoteDirection.Up: kind = extHostTypes.ChatResultFeedbackKind.Helpful; break; } @@ -342,7 +504,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { return; } - const ehAction = typeConvert.ChatAgentUserActionEvent.to(result, event, this.commands.converter); + const ehAction = typeConvert.ChatAgentUserActionEvent.to(result, event, this._commands.converter); if (ehAction) { agent.acceptAction(Object.freeze(ehAction)); } @@ -354,26 +516,36 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { return []; } + let disposables = this._completionDisposables.get(handle); + if (disposables) { + // Clear any disposables from the last invocation of this completion provider + disposables.clear(); + } else { + disposables = new DisposableStore(); + this._completionDisposables.set(handle, disposables); + } + const items = await agent.invokeCompletionProvider(query, token); - return items.map(typeConvert.ChatAgentCompletionItem.from); + + return items.map((i) => typeConvert.ChatAgentCompletionItem.from(i, this._commands.converter, disposables)); } - async $provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined> { + async $provideWelcomeMessage(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined> { const agent = this._agents.get(handle); if (!agent) { return; } - return await agent.provideWelcomeMessage(token); + return await agent.provideWelcomeMessage(typeConvert.ChatLocation.to(location), token); } - async $provideSampleQuestions(handle: number, token: CancellationToken): Promise { + async $provideSampleQuestions(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { return; } - return (await agent.provideSampleQuestions(token)) + return (await agent.provideSampleQuestions(typeConvert.ChatLocation.to(location), token)) .map(f => typeConvert.ChatFollowup.from(f, undefined)); } } @@ -381,13 +553,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { class ExtHostChatAgent { private _followupProvider: vscode.ChatFollowupProvider | undefined; - private _fullName: string | undefined; private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined; private _isDefault: boolean | undefined; private _helpTextPrefix: string | vscode.MarkdownString | undefined; private _helpTextVariablesPrefix: string | vscode.MarkdownString | undefined; private _helpTextPostfix: string | vscode.MarkdownString | undefined; - private _sampleRequest?: string; private _isSecondary: boolean | undefined; private _onDidReceiveFeedback = new Emitter(); private _onDidPerformAction = new Emitter(); @@ -395,6 +565,7 @@ class ExtHostChatAgent { private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _requester: vscode.ChatRequesterInformation | undefined; + private _supportsSlowReferences: boolean | undefined; constructor( public readonly extension: IExtensionDescription, @@ -436,11 +607,11 @@ class ExtHostChatAgent { .filter(f => !(f && 'message' in f)); } - async provideWelcomeMessage(token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined> { + async provideWelcomeMessage(location: vscode.ChatLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined> { if (!this._welcomeMessageProvider) { return []; } - const content = await this._welcomeMessageProvider.provideWelcomeMessage(token); + const content = await this._welcomeMessageProvider.provideWelcomeMessage(location, token); if (!content) { return []; } @@ -453,11 +624,11 @@ class ExtHostChatAgent { }); } - async provideSampleQuestions(token: CancellationToken): Promise { + async provideSampleQuestions(location: vscode.ChatLocation, token: CancellationToken): Promise { if (!this._welcomeMessageProvider || !this._welcomeMessageProvider.provideSampleQuestions) { return []; } - const content = await this._welcomeMessageProvider.provideSampleQuestions(token); + const content = await this._welcomeMessageProvider.provideSampleQuestions(location, token); if (!content) { return []; } @@ -478,7 +649,6 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : 'light' in this._iconPath ? this._iconPath.light : @@ -492,9 +662,9 @@ class ExtHostChatAgent { helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix), helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix), - sampleRequest: this._sampleRequest, supportIssueReporting: this._supportIssueReporting, - requester: this._requester + requester: this._requester, + supportsSlowVariables: this._supportsSlowReferences, }); updateScheduled = false; }); @@ -505,15 +675,6 @@ class ExtHostChatAgent { get id() { return that.id; }, - get fullName() { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - return that._fullName ?? that.extension.displayName ?? that.extension.name; - }, - set fullName(v) { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - that._fullName = v; - updateMetadataSoon(); - }, get iconPath() { return that._iconPath; }, @@ -580,19 +741,12 @@ class ExtHostChatAgent { that._isSecondary = v; updateMetadataSoon(); }, - get sampleRequest() { - return that._sampleRequest; - }, - set sampleRequest(v) { - that._sampleRequest = v; - updateMetadataSoon(); - }, get supportIssueReporting() { - checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); return that._supportIssueReporting; }, set supportIssueReporting(v) { - checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); that._supportIssueReporting = v; updateMetadataSoon(); }, @@ -607,9 +761,9 @@ class ExtHostChatAgent { throw new Error('triggerCharacters are required'); } - that._proxy.$registerAgentCompletionsProvider(that._handle, v.triggerCharacters); + that._proxy.$registerAgentCompletionsProvider(that._handle, that.id, v.triggerCharacters); } else { - that._proxy.$unregisterAgentCompletionsProvider(that._handle); + that._proxy.$unregisterAgentCompletionsProvider(that._handle, that.id); } }, get participantVariableProvider() { @@ -636,6 +790,15 @@ class ExtHostChatAgent { get requester() { return that._requester; }, + set supportsSlowReferences(v) { + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); + that._supportsSlowReferences = v; + updateMetadataSoon(); + }, + get supportsSlowReferences() { + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); + return that._supportsSlowReferences; + }, dispose() { disposed = true; that._followupProvider = undefined; @@ -645,7 +808,7 @@ class ExtHostChatAgent { } satisfies vscode.ChatParticipant; } - invoke(request: vscode.ChatRequest, context: vscode.ChatContext, response: vscode.ChatExtendedResponseStream, token: CancellationToken): vscode.ProviderResult { + invoke(request: vscode.ChatRequest, context: vscode.ChatContext, response: vscode.ChatResponseStream, token: CancellationToken): vscode.ProviderResult { return this._requestHandler(request, context, response, token); } } diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index 26fac7e95e0..dfc37201bd4 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostChatVariablesShape, IChatVariableResolverProgressDto, IMainContext, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; @@ -25,7 +26,7 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { this._proxy = mainContext.getProxy(MainContext.MainThreadChatVariables); } - async $resolveVariable(handle: number, requestId: string, messageText: string, token: CancellationToken): Promise { + async $resolveVariable(handle: number, requestId: string, messageText: string, token: CancellationToken): Promise { const item = this._resolver.get(handle); if (!item) { return undefined; @@ -35,13 +36,15 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { checkProposedApiEnabled(item.extension, 'chatParticipantAdditions'); const stream = new ChatVariableResolverResponseStream(requestId, this._proxy); const value = await item.resolver.resolve2(item.data.name, { prompt: messageText }, stream.apiObject, token); - if (value) { - return value.map(typeConvert.ChatVariable.from); + + // Temp, ignoring other returned values to convert the array into a single value + if (value && value[0]) { + return value[0].value; } } else { const value = await item.resolver.resolve(item.data.name, { prompt: messageText }, token); - if (value) { - return value.map(typeConvert.ChatVariable.from); + if (value && value[0]) { + return value[0].value; } } } catch (err) { @@ -50,10 +53,11 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { return undefined; } - registerVariableResolver(extension: IExtensionDescription, name: string, description: string, resolver: vscode.ChatVariableResolver): IDisposable { + registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, themeIconId?: string): IDisposable { const handle = ExtHostChatVariables._idPool++; - this._resolver.set(handle, { extension, data: { name, description }, resolver: resolver }); - this._proxy.$registerVariable(handle, { name, description }); + const icon = themeIconId ? ThemeIcon.fromId(themeIconId) : undefined; + this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription, icon }, resolver: resolver }); + this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow, fullName, icon }); return toDisposable(() => { this._resolver.delete(handle); @@ -96,14 +100,14 @@ class ChatVariableResolverResponseStream { progress(value) { throwIfDone(this.progress); const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.to(part); + const dto = typeConvert.ChatResponseProgressPart.from(part); _report(dto); return this; }, reference(value) { throwIfDone(this.reference); const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.ChatResponseReferencePart.to(part); + const dto = typeConvert.ChatResponseReferencePart.from(part); _report(dto); return this; }, @@ -111,9 +115,9 @@ class ChatVariableResolverResponseStream { throwIfDone(this.push); if (part instanceof extHostTypes.ChatResponseReferencePart) { - _report(typeConvert.ChatResponseReferencePart.to(part)); + _report(typeConvert.ChatResponseReferencePart.from(part)); } else if (part instanceof extHostTypes.ChatResponseProgressPart) { - _report(typeConvert.ChatResponseProgressPart.to(part)); + _report(typeConvert.ChatResponseProgressPart.from(part)); } return this; diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 586056f01a3..15730c390b9 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -292,7 +292,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { type ExtensionActionTelemetryMeta = { extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the extension handling the command, informing which extensions provide most-used functionality.' }; id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command, to understand which specific extension features are most popular.' }; - duration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The duration of the command execution, to detect performance issues' }; + duration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration of the command execution, to detect performance issues' }; owner: 'digitarald'; comment: 'Used to gain insight on the most popular commands used from extensions'; }; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 72cf9d3de72..05761198ee5 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -160,14 +160,14 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return commentController.value; } - async $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined): Promise { + async $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined, editorId?: string): Promise { const commentController = this._commentControllers.get(commentControllerHandle); if (!commentController) { return; } - commentController.$createCommentThreadTemplate(uriComponents, range); + commentController.$createCommentThreadTemplate(uriComponents, range, editorId); } async $setActiveComment(controllerHandle: number, commentInfo: { commentThreadHandle: number; uniqueIdInThread?: number }): Promise { @@ -215,7 +215,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } else if (rangesResult) { ranges = { ranges: rangesResult.ranges || [], - fileComments: rangesResult.fileComments || false + fileComments: rangesResult.enableFileComments || false }; } else { ranges = rangesResult ?? undefined; @@ -397,7 +397,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo private _commentsMap: Map = new Map(); - private _acceptInputDisposables = new MutableDisposable(); + private readonly _acceptInputDisposables = new MutableDisposable(); readonly value: vscode.CommentThread2; @@ -409,7 +409,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo private _range: vscode.Range | undefined, private _comments: vscode.Comment[], public readonly extensionDescription: IExtensionDescription, - private _isTemplate: boolean + private _isTemplate: boolean, + editorId?: string ) { this._acceptInputDisposables.value = new DisposableStore(); @@ -423,8 +424,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._id, this._uri, extHostTypeConverter.Range.from(this._range), + this._comments.map(cmt => convertToDTOComment(this, cmt, this._commentsMap, this.extensionDescription)), extensionDescription.identifier, - this._isTemplate + this._isTemplate, + editorId ); this._localDisposables = []; @@ -434,9 +437,6 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this.eventuallyUpdateCommentThread(); })); - // set up comments after ctor to batch update events. - this.comments = _comments; - this._localDisposables.push({ dispose: () => { proxy.$deleteCommentThread( @@ -463,6 +463,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set label(value: string | undefined) { that.label = value; }, get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return that.state; }, set state(value: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { that.state = value; }, + reveal: (comment?: vscode.Comment | vscode.CommentThreadRevealOptions, options?: vscode.CommentThreadRevealOptions) => that.reveal(comment, options), + hide: () => that.hide(), dispose: () => { that.dispose(); } @@ -546,6 +548,31 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return; } + async reveal(commentOrOptions?: vscode.Comment | vscode.CommentThreadRevealOptions, options?: vscode.CommentThreadRevealOptions): Promise { + checkProposedApiEnabled(this.extensionDescription, 'commentReveal'); + let comment: vscode.Comment | undefined; + if (commentOrOptions && (commentOrOptions as vscode.Comment).body !== undefined) { + comment = commentOrOptions as vscode.Comment; + } else { + options = options ?? commentOrOptions as vscode.CommentThreadRevealOptions; + } + let commentToReveal = comment ? this._commentsMap.get(comment) : undefined; + commentToReveal ??= this._commentsMap.get(this._comments[0])!; + let preserveFocus: boolean = true; + let focusReply: boolean = false; + if (options?.focus === types.CommentThreadFocus.Reply) { + focusReply = true; + preserveFocus = false; + } else if (options?.focus === types.CommentThreadFocus.Comment) { + preserveFocus = false; + } + return proxy.$revealCommentThread(this._commentControllerHandle, this.handle, commentToReveal, { preserveFocus, focusReply }); + } + + async hide(): Promise { + return proxy.$hideCommentThread(this._commentControllerHandle, this.handle); + } + dispose() { this._isDiposed = true; this._acceptInputDisposables.dispose(); @@ -614,11 +641,11 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return this._activeComment; } - private _activeThread: vscode.CommentThread2 | undefined; + private _activeThread: ExtHostCommentThread | undefined; get activeCommentThread(): vscode.CommentThread2 | undefined { checkProposedApiEnabled(this._extension, 'activeComment'); - return this._activeThread; + return this._activeThread?.value; } private _localDisposables: types.Disposable[]; @@ -680,8 +707,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } } - $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange | undefined): ExtHostCommentThread { - const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension, true); + $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange | undefined, editorId?: string): ExtHostCommentThread { + const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension, true, editorId); commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; this._threads.set(commentThread.handle, commentThread); return commentThread; diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index f3d7d2eb044..2808a4eb0b1 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -139,7 +139,7 @@ export class ExtHostConfigProvider { this._proxy = proxy; this._logService = logService; this._extHostWorkspace = extHostWorkspace; - this._configuration = Configuration.parse(data); + this._configuration = Configuration.parse(data, logService); this._configurationScopes = this._toMap(data.configurationScopes); } @@ -149,7 +149,7 @@ export class ExtHostConfigProvider { $acceptConfigurationChanged(data: IConfigurationInitData, change: IConfigurationChange) { const previous = { data: this._configuration.toData(), workspace: this._extHostWorkspace.workspace }; - this._configuration = Configuration.parse(data); + this._configuration = Configuration.parse(data, this._logService); this._configurationScopes = this._toMap(data.configurationScopes); this._onDidChangeConfiguration.fire(this._toConfigurationChangeEvent(change, previous)); } @@ -319,7 +319,7 @@ export class ExtHostConfigProvider { } private _toConfigurationChangeEvent(change: IConfigurationChange, previous: { data: IConfigurationData; workspace: Workspace | undefined }): vscode.ConfigurationChangeEvent { - const event = new ConfigurationChangeEvent(change, previous, this._configuration, this._extHostWorkspace.workspace); + const event = new ConfigurationChangeEvent(change, previous, this._configuration, this._extHostWorkspace.workspace, this._logService); return Object.freeze({ affectsConfiguration: (section: string, scope?: vscode.ConfigurationScope) => event.affectsConfiguration(section, scopeToOverrides(scope)) }); diff --git a/src/vs/workbench/api/common/extHostConsoleForwarder.ts b/src/vs/workbench/api/common/extHostConsoleForwarder.ts index 767d73b51c8..663841771e7 100644 --- a/src/vs/workbench/api/common/extHostConsoleForwarder.ts +++ b/src/vs/workbench/api/common/extHostConsoleForwarder.ts @@ -107,7 +107,7 @@ function safeStringifyArgumentsToArray(args: IArguments, includeStack: boolean): if (includeStack) { const stack = new Error().stack; if (stack) { - argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') } as IStackArgument); + argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') } satisfies IStackArgument); } } diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index 2bf237093b7..99eef8fbc8b 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -133,22 +133,22 @@ class EditorProviderStore { private readonly _providers = new Map(); public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable { - return this.add(CustomEditorType.Text, viewType, extension, provider); + return this.add(viewType, { type: CustomEditorType.Text, extension, provider }); } public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable { - return this.add(CustomEditorType.Custom, viewType, extension, provider); + return this.add(viewType, { type: CustomEditorType.Custom, extension, provider }); } public get(viewType: string): ProviderEntry | undefined { return this._providers.get(viewType); } - private add(type: CustomEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider): vscode.Disposable { + private add(viewType: string, entry: ProviderEntry): vscode.Disposable { if (this._providers.has(viewType)) { throw new Error(`Provider for viewType:${viewType} already registered`); } - this._providers.set(viewType, { type, extension, provider } as ProviderEntry); + this._providers.set(viewType, entry); return new extHostTypes.Disposable(() => this._providers.delete(viewType)); } } diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index e38d3ee11b1..a5e22aa3dee 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -7,7 +7,8 @@ import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Disposable as DisposableCls, toDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ISignService } from 'vs/platform/sign/common/sign'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -15,7 +16,7 @@ import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThre import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, Thread, StackFrame, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; +import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, DebugThread, DebugStackFrame, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebugVisualization, IDebugVisualizationContext, IDebuggerContribution, DebugVisualizationType, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; @@ -25,10 +26,11 @@ import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import { IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostVariableResolverProvider } from './extHostVariableResolverService'; -import { toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; +import { coalesce } from 'vs/base/common/arrays'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -44,8 +46,8 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { onDidReceiveDebugSessionCustomEvent: Event; onDidChangeBreakpoints: Event; breakpoints: vscode.Breakpoint[]; - onDidChangeActiveStackItem: Event; - activeStackItem: vscode.Thread | vscode.StackFrame | undefined; + onDidChangeActiveStackItem: Event; + activeStackItem: vscode.DebugThread | vscode.DebugStackFrame | undefined; addBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; removeBreakpoints(breakpoints0: readonly vscode.Breakpoint[]): Promise; @@ -59,7 +61,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri; } -export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDebugServiceShape { +export abstract class ExtHostDebugServiceBase extends DisposableCls implements IExtHostDebugService, ExtHostDebugServiceShape { readonly _serviceBrand: undefined; @@ -97,8 +99,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private readonly _onDidChangeBreakpoints: Emitter; - private _activeStackItem: vscode.Thread | vscode.StackFrame | undefined; - private readonly _onDidChangeActiveStackItem: Emitter; + private _activeStackItem: vscode.DebugThread | vscode.DebugStackFrame | undefined; + private readonly _onDidChangeActiveStackItem: Emitter; private _debugAdapters: Map; private _debugAdaptersTrackers: Map; @@ -122,7 +124,10 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider, @IExtHostCommands private _commands: IExtHostCommands, + @IExtHostTesting private _testing: IExtHostTesting, ) { + super(); + this._configProviderHandleCounter = 0; this._configProviders = []; @@ -135,25 +140,25 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E this._debugAdapters = new Map(); this._debugAdaptersTrackers = new Map(); - this._onDidStartDebugSession = new Emitter(); - this._onDidTerminateDebugSession = new Emitter(); - this._onDidChangeActiveDebugSession = new Emitter(); - this._onDidReceiveDebugSessionCustomEvent = new Emitter(); + this._onDidStartDebugSession = this._register(new Emitter()); + this._onDidTerminateDebugSession = this._register(new Emitter()); + this._onDidChangeActiveDebugSession = this._register(new Emitter()); + this._onDidReceiveDebugSessionCustomEvent = this._register(new Emitter()); this._debugServiceProxy = extHostRpcService.getProxy(MainContext.MainThreadDebugService); - this._onDidChangeBreakpoints = new Emitter(); + this._onDidChangeBreakpoints = this._register(new Emitter()); - this._onDidChangeActiveStackItem = new Emitter(); + this._onDidChangeActiveStackItem = this._register(new Emitter()); this._activeDebugConsole = new ExtHostDebugConsole(this._debugServiceProxy); this._breakpoints = new Map(); this._extensionService.getExtensionRegistry().then((extensionRegistry: ExtensionDescriptionRegistry) => { - extensionRegistry.onDidChange(_ => { + this._register(extensionRegistry.onDidChange(_ => { this.registerAllDebugTypes(extensionRegistry); - }); + })); this.registerAllDebugTypes(extensionRegistry); }); } @@ -168,7 +173,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return item ? this.convertVisualizerTreeItem(treeId, item) : undefined; } - public registerDebugVisualizationTree(manifest: Readonly, id: string, provider: vscode.DebugVisualizationTree): vscode.Disposable { + public registerDebugVisualizationTree(manifest: IExtensionDescription, id: string, provider: vscode.DebugVisualizationTree): vscode.Disposable { const extensionId = ExtensionIdentifier.toKey(manifest.identifier); const key = this.extensionVisKey(extensionId, id); if (this._debugVisualizationProviders.has(key)) { @@ -278,11 +283,11 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E // extension debug API - get activeStackItem(): vscode.Thread | vscode.StackFrame | undefined { + get activeStackItem(): vscode.DebugThread | vscode.DebugStackFrame | undefined { return this._activeStackItem; } - get onDidChangeActiveStackItem(): Event { + get onDidChangeActiveStackItem(): Event { return this._onDidChangeActiveStackItem.event; } @@ -412,11 +417,11 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E if (bp instanceof SourceBreakpoint) { let dto = map.get(bp.location.uri.toString()); if (!dto) { - dto = { + dto = { type: 'sourceMulti', uri: bp.location.uri, lines: [] - }; + } satisfies ISourceMultiBreakpointDto; map.set(bp.location.uri.toString(), dto); dtos.push(dto); } @@ -463,6 +468,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise { + const testRunMeta = options.testRun && this._testing.getMetadataForRun(options.testRun); + return this._debugServiceProxy.$startDebugging(folder ? folder.uri : undefined, nameOrConfig, { parentSessionID: options.parentSession ? options.parentSession.id : undefined, lifecycleManagedByParent: options.lifecycleManagedByParent, @@ -470,6 +477,10 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E noDebug: options.noDebug, compact: options.compact, suppressSaveBeforeStart: options.suppressSaveBeforeStart, + testRun: testRunMeta && { + runId: testRunMeta.runId, + taskId: testRunMeta.taskId, + }, // Check debugUI for back-compat, #147264 suppressDebugStatusbar: options.suppressDebugStatusbar ?? (options as any).debugUI?.simple, @@ -769,13 +780,13 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } public async $acceptStackFrameFocus(focusDto: IThreadFocusDto | IStackFrameFocusDto | undefined): Promise { - let focus: vscode.Thread | vscode.StackFrame | undefined; + let focus: vscode.DebugThread | vscode.DebugStackFrame | undefined; if (focusDto) { const session = await this.getSession(focusDto.sessionId); if (focusDto.kind === 'thread') { - focus = new Thread(session.api, focusDto.threadId); + focus = new DebugThread(session.api, focusDto.threadId); } else { - focus = new StackFrame(session.api, focusDto.threadId, focusDto.frameId); + focus = new DebugStackFrame(session.api, focusDto.threadId, focusDto.frameId); } } @@ -882,28 +893,28 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private convertToDto(x: vscode.DebugAdapterDescriptor): Dto { if (x instanceof DebugAdapterExecutable) { - return { + return { type: 'executable', command: x.command, args: x.args, options: x.options - }; + } satisfies IDebugAdapterExecutable; } else if (x instanceof DebugAdapterServer) { - return { + return { type: 'server', port: x.port, host: x.host - }; + } satisfies IDebugAdapterServer; } else if (x instanceof DebugAdapterNamedPipeServer) { - return { + return { type: 'pipeServer', path: x.path - }; + } satisfies IDebugAdapterNamedPipeServer; } else if (x instanceof DebugAdapterInlineImplementation) { - return >{ + return { type: 'implementation', implementation: x.implementation - }; + } as Dto; } else { throw new Error('convertToDto unexpected type'); } @@ -935,7 +946,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private definesDebugType(ed: IExtensionDescription, type: string) { if (ed.contributes) { - const debuggers = ed.contributes['debuggers']; + const debuggers = ed.contributes['debuggers']; if (debuggers && debuggers.length > 0) { for (const dbg of debuggers) { // only debugger contributions with a "label" are considered a "defining" debugger contribution @@ -961,7 +972,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return Promise.race([ Promise.all(promises).then(result => { - const trackers = result.filter(t => !!t); // filter null + const trackers = coalesce(result); // filter null if (trackers.length > 0) { return new MultiTracker(trackers); } @@ -1244,8 +1255,9 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, - @IExtHostCommands commands: IExtHostCommands + @IExtHostCommands commands: IExtHostCommands, + @IExtHostTesting testing: IExtHostTesting, ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } } diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index e23ce395cc8..da9a087fbb1 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -234,7 +234,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private static _idPool: number = 0; private static readonly _maxDiagnosticsPerFile: number = 1000; - private static readonly _maxDiagnosticsTotal: number = 1.1 * ExtHostDiagnostics._maxDiagnosticsPerFile; + private static readonly _maxDiagnosticsTotal: number = 1.1 * this._maxDiagnosticsPerFile; private readonly _proxy: MainThreadDiagnosticsShape; private readonly _collections = new Map(); diff --git a/src/vs/workbench/api/common/extHostDialogs.ts b/src/vs/workbench/api/common/extHostDialogs.ts index c33cb0704dd..372037aa341 100644 --- a/src/vs/workbench/api/common/extHostDialogs.ts +++ b/src/vs/workbench/api/common/extHostDialogs.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { MainContext, MainThreadDiaglogsShape, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostDialogs { @@ -17,7 +17,7 @@ export class ExtHostDialogs { this._proxy = mainContext.getProxy(MainContext.MainThreadDialogs); } - showOpenDialog(extension: IRelaxedExtensionDescription, options?: vscode.OpenDialogOptions): Promise { + showOpenDialog(extension: IExtensionDescription, options?: vscode.OpenDialogOptions): Promise { if (options?.allowUIResources) { checkProposedApiEnabled(extension, 'showLocal'); } diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index ee321b2575a..b3edbad94a1 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -76,6 +76,9 @@ export class ExtHostDocumentData extends MirrorTextModel { validateRange(ran) { return that._validateRange(ran); }, validatePosition(pos) { return that._validatePosition(pos); }, getWordRangeAtPosition(pos, regexp?) { return that._getWordRangeAtPosition(pos, regexp); }, + [Symbol.for('debug.description')]() { + return `TextDocument(${that._uri.toString()})`; + } }; } return Object.freeze(this._document); diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index 7a90e5db0f9..de3123da9de 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -15,6 +15,7 @@ import type * as vscode from 'vscode'; import { LinkedList } from 'vs/base/common/linkedList'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; type Listener = [Function, any, IExtensionDescription]; @@ -165,7 +166,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic } if (version === document.version) { - return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto); + return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers(dto)); } return Promise.reject(new Error('concurrent_edits')); diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index d0e035825a6..3f89c17b81f 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -99,7 +99,7 @@ class ExtHostEditorTab { case TabInputKind.InteractiveEditorInput: return new InteractiveWindowInput(URI.revive(this._dto.input.uri), URI.revive(this._dto.input.inputBoxUri)); case TabInputKind.ChatEditorInput: - return new ChatEditorTabInput(this._dto.input.providerId); + return new ChatEditorTabInput(); case TabInputKind.MultiDiffEditorInput: return new TextMultiDiffTabInput(this._dto.input.diffEditors.map(diff => new TextDiffTabInput(URI.revive(diff.original), URI.revive(diff.modified)))); default: diff --git a/src/vs/workbench/api/common/extHostEmbedding.ts b/src/vs/workbench/api/common/extHostEmbedding.ts new file mode 100644 index 00000000000..4c712aee9e8 --- /dev/null +++ b/src/vs/workbench/api/common/extHostEmbedding.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 { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostEmbeddingsShape, IMainContext, MainContext, MainThreadEmbeddingsShape } from 'vs/workbench/api/common/extHost.protocol'; +import type * as vscode from 'vscode'; + + +export class ExtHostEmbeddings implements ExtHostEmbeddingsShape { + + private readonly _proxy: MainThreadEmbeddingsShape; + private readonly _provider = new Map(); + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private _allKnownModels = new Set(); + private _handlePool: number = 0; + + constructor( + mainContext: IMainContext + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadEmbeddings); + } + + registerEmbeddingsProvider(_extension: IExtensionDescription, embeddingsModel: string, provider: vscode.EmbeddingsProvider): IDisposable { + if (this._allKnownModels.has(embeddingsModel)) { + throw new Error('An embeddings provider for this model is already registered'); + } + + const handle = this._handlePool++; + + this._proxy.$registerEmbeddingProvider(handle, embeddingsModel); + this._provider.set(handle, { id: embeddingsModel, provider }); + + return toDisposable(() => { + this._allKnownModels.delete(embeddingsModel); + this._proxy.$unregisterEmbeddingProvider(handle); + this._provider.delete(handle); + }); + } + + async computeEmbeddings(embeddingsModel: string, input: string, token?: vscode.CancellationToken): Promise; + async computeEmbeddings(embeddingsModel: string, input: string[], token?: vscode.CancellationToken): Promise; + async computeEmbeddings(embeddingsModel: string, input: string | string[], token?: vscode.CancellationToken): Promise { + + token ??= CancellationToken.None; + + let returnSingle = false; + if (typeof input === 'string') { + input = [input]; + returnSingle = true; + } + const result = await this._proxy.$computeEmbeddings(embeddingsModel, input, token); + if (result.length !== input.length) { + throw new Error(); + } + if (returnSingle) { + if (result.length !== 1) { + throw new Error(); + } + return result[0]; + } + return result; + + } + + async $provideEmbeddings(handle: number, input: string[], token: CancellationToken): Promise<{ values: number[] }[]> { + const data = this._provider.get(handle); + if (!data) { + return []; + } + const result = await data.provider.provideEmbeddings(input, token); + if (!result) { + return []; + } + return result; + } + + get embeddingsModels(): string[] { + return Array.from(this._allKnownModels); + } + + $acceptEmbeddingModels(models: string[]): void { + this._allKnownModels = new Set(models); + this._onDidChange.fire(); + } +} diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index 881140392bd..d6b20d6103c 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -5,7 +5,7 @@ import type * as vscode from 'vscode'; import * as errors from 'vs/base/common/errors'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ExtensionIdentifier, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; @@ -28,10 +28,10 @@ export interface IExtensionAPI { } export type ExtensionActivationTimesFragment = { - startup?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Activation occurred during startup' }; - codeLoadingTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time it took to load the extension\'s code' }; - activateCallTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time it took to call activate' }; - activateResolvedTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time it took for async-activation to finish' }; + startup?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Activation occurred during startup' }; + codeLoadingTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took to load the extension\'s code' }; + activateCallTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took to call activate' }; + activateResolvedTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took for async-activation to finish' }; }; export class ExtensionActivationTimes { @@ -119,7 +119,7 @@ export class ActivatedExtension { public readonly activationTimes: ExtensionActivationTimes; public readonly module: IExtensionModule; public readonly exports: IExtensionAPI | undefined; - public readonly subscriptions: IDisposable[]; + public readonly disposable: IDisposable; constructor( activationFailed: boolean, @@ -127,32 +127,32 @@ export class ActivatedExtension { activationTimes: ExtensionActivationTimes, module: IExtensionModule, exports: IExtensionAPI | undefined, - subscriptions: IDisposable[] + disposable: IDisposable ) { this.activationFailed = activationFailed; this.activationFailedError = activationFailedError; this.activationTimes = activationTimes; this.module = module; this.exports = exports; - this.subscriptions = subscriptions; + this.disposable = disposable; } } export class EmptyExtension extends ActivatedExtension { constructor(activationTimes: ExtensionActivationTimes) { - super(false, null, activationTimes, { activate: undefined, deactivate: undefined }, undefined, []); + super(false, null, activationTimes, { activate: undefined, deactivate: undefined }, undefined, Disposable.None); } } export class HostExtension extends ActivatedExtension { constructor() { - super(false, null, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, []); + super(false, null, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None); } } class FailedExtension extends ActivatedExtension { constructor(activationError: Error) { - super(true, activationError, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, []); + super(true, activationError, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None); } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 2679364d945..97ecf09ea56 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -10,7 +10,7 @@ import * as path from 'vs/base/common/path'; import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { asPromise, Barrier, IntervalTimer, timeout } from 'vs/base/common/async'; -import { dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; @@ -24,7 +24,7 @@ import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, is import { ExtensionDescriptionRegistry, IActivationEventsReader } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; -import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { VSBuffer } from 'vs/base/common/buffer'; import { ExtensionGlobalMemento, ExtensionMemento } from 'vs/workbench/api/common/extHostMemento'; import { RemoteAuthorityResolverError, ExtensionKind, ExtensionMode, ExtensionRuntime, ManagedResolvedAuthority as ExtHostManagedResolvedAuthority } from 'vs/workbench/api/common/extHostTypes'; @@ -75,7 +75,7 @@ type TelemetryActivationEventFragment = { extensionVersion: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The version of the extension' }; publisherDisplayName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The publisher of the extension' }; activationEvents: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'All activation events of the extension' }; - isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'If the extension is builtin or git installed' }; + isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the extension is builtin or git installed' }; reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The activation event' }; reasonId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the activation event' }; }; @@ -177,10 +177,10 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; - this._instaService = instaService.createChild(new ServiceCollection( + this._instaService = this._store.add(instaService.createChild(new ServiceCollection( [IExtHostStorage, this._storage], [IExtHostSecretState, this._secretState] - )); + ))); this._activator = this._register(new ExtensionsActivator( this._myRegistry, @@ -409,9 +409,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // clean up subscriptions try { - dispose(extension.subscriptions); + extension.disposable.dispose(); } catch (err) { - this._logService.error(`An error occurred when deactivating the subscriptions for extension '${extensionId.value}':`); + this._logService.error(`An error occurred when disposing the subscriptions for extension '${extensionId.value}':`); this._logService.error(err); } @@ -482,25 +482,26 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._logService.info(`ExtensionService#_doActivateExtension ${extensionDescription.identifier.value}, startup: ${reason.startup}, activationEvent: '${reason.activationEvent}'${extensionDescription.identifier.value !== reason.extensionId.value ? `, root cause: ${reason.extensionId.value}` : ``}`); this._logService.flush(); + const extensionInternalStore = new DisposableStore(); // disposables that follow the extension lifecycle const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ this._loadCommonJSModule(extensionDescription, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), - this._loadExtensionContext(extensionDescription) + this._loadExtensionContext(extensionDescription, extensionInternalStore) ]).then(values => { performance.mark(`code/extHost/willActivateExtension/${extensionDescription.identifier.value}`); - return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); + return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], extensionInternalStore, activationTimesBuilder); }).then((activatedExtension) => { performance.mark(`code/extHost/didActivateExtension/${extensionDescription.identifier.value}`); return activatedExtension; }); } - private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { + private _loadExtensionContext(extensionDescription: IExtensionDescription, extensionInternalStore: DisposableStore): Promise { - const lanuageModelAccessInformation = this._extHostLanguageModels.createLanguageModelAccessInformation(extensionDescription); - const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage); - const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); - const secrets = new ExtensionSecrets(extensionDescription, this._secretState); + const languageModelAccessInformation = this._extHostLanguageModels.createLanguageModelAccessInformation(extensionDescription); + const globalState = extensionInternalStore.add(new ExtensionGlobalMemento(extensionDescription, this._storage)); + const workspaceState = extensionInternalStore.add(new ExtensionMemento(extensionDescription.identifier.value, false, this._storage)); + const secrets = extensionInternalStore.add(new ExtensionSecrets(extensionDescription, this._secretState)); const extensionMode = extensionDescription.isUnderDevelopment ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) : ExtensionMode.Production; @@ -526,7 +527,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme workspaceState, secrets, subscriptions: [], - get languageModelAccessInformation() { return lanuageModelAccessInformation; }, + get languageModelAccessInformation() { return languageModelAccessInformation; }, get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, @@ -568,7 +569,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }); } - private static _callActivate(logService: ILogService, extensionId: ExtensionIdentifier, extensionModule: IExtensionModule, context: vscode.ExtensionContext, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + private static _callActivate(logService: ILogService, extensionId: ExtensionIdentifier, extensionModule: IExtensionModule, context: vscode.ExtensionContext, extensionInternalStore: IDisposable, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { // Make sure the extension's surface is not undefined extensionModule = extensionModule || { activate: undefined, @@ -576,7 +577,10 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }; return this._callActivateOptional(logService, extensionId, extensionModule, context, activationTimesBuilder).then((extensionExports) => { - return new ActivatedExtension(false, null, activationTimesBuilder.build(), extensionModule, extensionExports, context.subscriptions); + return new ActivatedExtension(false, null, activationTimesBuilder.build(), extensionModule, extensionExports, toDisposable(() => { + extensionInternalStore.dispose(); + dispose(context.subscriptions); + })); }); } @@ -615,7 +619,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }); } - private _activateAllStartupFinishedDeferred(extensions: Readonly[], start: number = 0): void { + private _activateAllStartupFinishedDeferred(extensions: IExtensionDescription[], start: number = 0): void { const timeBudget = 50; // 50 milliseconds const startTime = Date.now(); @@ -1230,7 +1234,7 @@ class SyncedActivationEventsReader implements IActivationEventsReader { this.addActivationEvents(activationEvents); } - public readActivationEvents(extensionDescription: Readonly): string[] { + public readActivationEvents(extensionDescription: IExtensionDescription): string[] { return this._map.get(extensionDescription.identifier) ?? []; } diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index a1c9a2bdaf4..5ad160530c9 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -12,7 +12,7 @@ import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, Sou import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { FileOperation } from 'vs/platform/files/common/files'; +import { FileChangeFilter, FileOperation } from 'vs/platform/files/common/files'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; @@ -122,6 +122,10 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { return disposable; // workspace is already watched by default, no need to watch again! } + if (options?.ignoreChangeEvents && options?.ignoreCreateEvents && options?.ignoreDeleteEvents) { + return disposable; // no need to watch if we ignore all events + } + const proxy = mainContext.getProxy(MainContext.MainThreadFileSystemEventService); let recursive = false; @@ -129,7 +133,26 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { recursive = true; // only watch recursively if pattern indicates the need for it } - proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes: options?.excludes ?? [] }, Boolean(correlate)); + let filter: FileChangeFilter | undefined; + if (correlate) { + if (options?.ignoreChangeEvents || options?.ignoreCreateEvents || options?.ignoreDeleteEvents) { + filter = FileChangeFilter.UPDATED | FileChangeFilter.ADDED | FileChangeFilter.DELETED; + + if (options?.ignoreChangeEvents) { + filter &= ~FileChangeFilter.UPDATED; + } + + if (options?.ignoreCreateEvents) { + filter &= ~FileChangeFilter.ADDED; + } + + if (options?.ignoreDeleteEvents) { + filter &= ~FileChangeFilter.DELETED; + } + } + } + + proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes: options?.excludes ?? [], filter }, Boolean(correlate)); return Disposable.from({ dispose: () => proxy.$unwatch(this.session) }); } diff --git a/src/vs/workbench/api/common/extHostInlineChat.ts b/src/vs/workbench/api/common/extHostInlineChat.ts deleted file mode 100644 index efd9ee8b248..00000000000 --- a/src/vs/workbench/api/common/extHostInlineChat.ts +++ /dev/null @@ -1,260 +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 { raceCancellation } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { toDisposable } from 'vs/base/common/lifecycle'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { IPosition } from 'vs/editor/common/core/position'; -import { IRange } from 'vs/editor/common/core/range'; -import { ISelection } from 'vs/editor/common/core/selection'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostInlineChatShape, IInlineChatResponseDto, IMainContext, MainContext, MainThreadInlineChatShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IInlineChatFollowup, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import type * as vscode from 'vscode'; - -class ProviderWrapper { - - private static _pool = 0; - - readonly handle: number = ProviderWrapper._pool++; - - constructor( - readonly extension: Readonly, - readonly provider: vscode.InteractiveEditorSessionProvider - ) { } -} - -class SessionWrapper { - - readonly responses: (vscode.InteractiveEditorResponse | vscode.InteractiveEditorMessageResponse)[] = []; - - constructor( - readonly session: vscode.InteractiveEditorSession - ) { } -} - -export class ExtHostInteractiveEditor implements ExtHostInlineChatShape { - - private static _nextId = 0; - - private readonly _inputProvider = new Map(); - private readonly _inputSessions = new Map(); - private readonly _proxy: MainThreadInlineChatShape; - - constructor( - mainContext: IMainContext, - extHostCommands: ExtHostCommands, - private readonly _documents: ExtHostDocuments, - private readonly _logService: ILogService, - ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadInlineChat); - - type EditorChatApiArg = { - initialRange?: vscode.Range; - initialSelection?: vscode.Selection; - message?: string; - autoSend?: boolean; - position?: vscode.Position; - }; - - type InteractiveEditorRunOptions = { - initialRange?: IRange; - initialSelection?: ISelection; - message?: string; - autoSend?: boolean; - position?: IPosition; - }; - - extHostCommands.registerApiCommand(new ApiCommand( - 'vscode.editorChat.start', 'inlineChat.start', 'Invoke a new editor chat session', - [new ApiCommandArgument('Run arguments', '', _v => true, v => { - - if (!v) { - return undefined; - } - - return { - initialRange: v.initialRange ? typeConvert.Range.from(v.initialRange) : undefined, - initialSelection: extHostTypes.Selection.isSelection(v.initialSelection) ? typeConvert.Selection.from(v.initialSelection) : undefined, - message: v.message, - autoSend: v.autoSend, - position: v.position ? typeConvert.Position.from(v.position) : undefined, - }; - })], - ApiCommandResult.Void - )); - } - - registerProvider(extension: Readonly, provider: vscode.InteractiveEditorSessionProvider, metadata?: vscode.InteractiveEditorSessionProviderMetadata): vscode.Disposable { - const wrapper = new ProviderWrapper(extension, provider); - this._inputProvider.set(wrapper.handle, wrapper); - this._proxy.$registerInteractiveEditorProvider(wrapper.handle, metadata?.label ?? extension.displayName ?? extension.name, extension.identifier, typeof provider.handleInteractiveEditorResponseFeedback === 'function', typeof provider.provideFollowups === 'function', metadata?.supportReportIssue ?? false); - return toDisposable(() => { - this._proxy.$unregisterInteractiveEditorProvider(wrapper.handle); - this._inputProvider.delete(wrapper.handle); - }); - } - - async $prepareSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise { - const entry = this._inputProvider.get(handle); - if (!entry) { - this._logService.warn('CANNOT prepare session because the PROVIDER IS GONE'); - return undefined; - } - - const document = this._documents.getDocument(URI.revive(uri)); - const selection = typeConvert.Selection.to(range); - const session = await entry.provider.prepareInteractiveEditorSession({ document, selection }, token); - if (!session) { - return undefined; - } - - if (session.wholeRange && !session.wholeRange.contains(selection)) { - throw new Error(`InteractiveEditorSessionProvider returned a wholeRange that does not contain the selection.`); - } - - const id = ExtHostInteractiveEditor._nextId++; - this._inputSessions.set(id, new SessionWrapper(session)); - - return { - id, - placeholder: session.placeholder, - input: session.input, - slashCommands: session.slashCommands?.map(c => ({ command: c.command, detail: c.detail, refer: c.refer, executeImmediately: c.executeImmediately })), - wholeRange: typeConvert.Range.from(session.wholeRange), - message: session.message - }; - } - - async $provideResponse(handle: number, item: IInlineChatSession, request: IInlineChatRequest, token: CancellationToken): Promise { - const entry = this._inputProvider.get(handle); - if (!entry) { - return undefined; - } - const sessionData = this._inputSessions.get(item.id); - if (!sessionData) { - return; - } - - const apiRequest: vscode.InteractiveEditorRequest = { - prompt: request.prompt, - selection: typeConvert.Selection.to(request.selection), - wholeRange: typeConvert.Range.to(request.wholeRange), - attempt: request.attempt, - live: request.live, - previewDocument: this._documents.getDocument(URI.revive(request.previewDocument)), - withIntentDetection: request.withIntentDetection, - }; - - - let done = false; - const progress: vscode.Progress = { - report: async value => { - if (!request.live && value.edits?.length) { - throw new Error('Progress reporting is only supported for live sessions'); - } - if (done || token.isCancellationRequested) { - return; - } - await this._proxy.$handleProgressChunk(request.requestId, { - message: value.message, - edits: value.edits?.map(typeConvert.TextEdit.from), - editsShouldBeInstant: value.editsShouldBeInstant, - slashCommand: value.slashCommand?.command, - markdownFragment: extHostTypes.MarkdownString.isMarkdownString(value.content) ? value.content.value : value.content - }); - } - }; - - const task = Promise.resolve(entry.provider.provideInteractiveEditorResponse(sessionData.session, apiRequest, progress, token)); - - let res: vscode.InteractiveEditorResponse | vscode.InteractiveEditorMessageResponse | null | undefined; - try { - res = await raceCancellation(task, token); - } finally { - done = true; - } - - if (!res) { - return undefined; - } - - - const id = sessionData.responses.push(res) - 1; - - const stub: Partial = { - wholeRange: typeConvert.Range.from(res.wholeRange), - placeholder: res.placeholder, - }; - - if (!ExtHostInteractiveEditor._isEditResponse(res)) { - return { - ...stub, - id, - type: InlineChatResponseType.EditorEdit, - message: typeConvert.MarkdownString.from(res.contents), - edits: [] - }; - } - - const { edits, contents } = res; - const message = contents !== undefined ? typeConvert.MarkdownString.from(contents) : undefined; - if (edits instanceof extHostTypes.WorkspaceEdit) { - return { - ...stub, - id, - type: InlineChatResponseType.BulkEdit, - edits: typeConvert.WorkspaceEdit.from(edits), - message - }; - - } else { - return { - ...stub, - id, - type: InlineChatResponseType.EditorEdit, - edits: (edits).map(typeConvert.TextEdit.from), - message - }; - } - } - - async $provideFollowups(handle: number, sessionId: number, responseId: number, token: CancellationToken): Promise { - const entry = this._inputProvider.get(handle); - const sessionData = this._inputSessions.get(sessionId); - const response = sessionData?.responses[responseId]; - if (entry && response && entry.provider.provideFollowups) { - const task = Promise.resolve(entry.provider.provideFollowups(sessionData.session, response, token)); - const followups = await raceCancellation(task, token); - return followups?.map(typeConvert.ChatInlineFollowup.from); - } - return undefined; - } - - - $handleFeedback(handle: number, sessionId: number, responseId: number, kind: InlineChatResponseFeedbackKind): void { - const entry = this._inputProvider.get(handle); - const sessionData = this._inputSessions.get(sessionId); - const response = sessionData?.responses[responseId]; - if (entry && response) { - const apiKind = typeConvert.InteractiveEditorResponseFeedbackKind.to(kind); - entry.provider.handleInteractiveEditorResponseFeedback?.(sessionData.session, response, apiKind); - } - } - - $releaseSession(handle: number, sessionId: number) { - // TODO@jrieken remove this - } - - private static _isEditResponse(thing: any): thing is vscode.InteractiveEditorResponse { - return typeof thing === 'object' && typeof (thing).edits === 'object'; - } -} diff --git a/src/vs/workbench/api/common/extHostIssueReporter.ts b/src/vs/workbench/api/common/extHostIssueReporter.ts deleted file mode 100644 index b70e0093645..00000000000 --- a/src/vs/workbench/api/common/extHostIssueReporter.ts +++ /dev/null @@ -1,95 +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 'vs/base/common/cancellation'; -import { UriComponents } from 'vs/base/common/uri'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtHostIssueReporterShape, IMainContext, MainContext, MainThreadIssueReporterShape } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable } from 'vs/workbench/api/common/extHostTypes'; -import type { IssueDataProvider, IssueUriRequestHandler } from 'vscode'; - -export class ExtHostIssueReporter implements ExtHostIssueReporterShape { - private _IssueUriRequestHandlers: Map = new Map(); - private _IssueDataProviders: Map = new Map(); - - private readonly _proxy: MainThreadIssueReporterShape; - - constructor( - mainContext: IMainContext - ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadIssueReporter); - } - - async $getIssueReporterUri(extensionId: string, token: CancellationToken): Promise { - if (this._IssueUriRequestHandlers.size === 0) { - throw new Error('No issue request handlers registered'); - } - - const provider = this._IssueUriRequestHandlers.get(extensionId); - if (!provider) { - throw new Error('Issue request handler not found'); - } - - const result = await provider.handleIssueUrlRequest(); - if (!result) { - throw new Error('Issue request handler returned no result'); - } - return result; - } - - async $getIssueReporterData(extensionId: string, token: CancellationToken): Promise { - if (this._IssueDataProviders.size === 0) { - throw new Error('No issue request handlers registered'); - } - - const provider = this._IssueDataProviders.get(extensionId); - if (!provider) { - throw new Error('Issue data provider not found'); - } - - const result = await provider.provideIssueData(token); - if (!result) { - throw new Error('Issue data provider returned no result'); - } - return result; - } - - async $getIssueReporterTemplate(extensionId: string, token: CancellationToken): Promise { - if (this._IssueDataProviders.size === 0) { - throw new Error('No issue request handlers registered'); - } - - const provider = this._IssueDataProviders.get(extensionId); - if (!provider) { - throw new Error('Issue data provider not found'); - } - - const result = await provider.provideIssueTemplate(token); - if (!result) { - throw new Error('Issue template provider returned no result'); - } - return result; - } - - registerIssueUriRequestHandler(extension: IExtensionDescription, provider: IssueUriRequestHandler): Disposable { - const extensionId = extension.identifier.value; - this._IssueUriRequestHandlers.set(extensionId, provider); - this._proxy.$registerIssueUriRequestHandler(extensionId); - return new Disposable(() => { - this._proxy.$unregisterIssueUriRequestHandler(extensionId); - this._IssueUriRequestHandlers.delete(extensionId); - }); - } - - registerIssueDataProvider(extension: IExtensionDescription, provider: IssueDataProvider): Disposable { - const extensionId = extension.identifier.value; - this._IssueDataProviders.set(extensionId, provider); - this._proxy.$registerIssueDataProvider(extensionId); - return new Disposable(() => { - this._proxy.$unregisterIssueDataProvider(extensionId); - this._IssueDataProviders.delete(extensionId); - }); - } -} diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index fe84fef3af9..50ab3ea6729 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -32,8 +32,8 @@ import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { CodeActionKind, CompletionList, Disposable, DocumentPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes'; +import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; import { Cache } from './cache'; import * as extHostProtocol from './extHost.protocol'; @@ -255,17 +255,33 @@ class TypeDefinitionAdapter { class HoverAdapter { + private _hoverCounter: number = 0; + private _hoverMap: Map = new Map(); + + private static HOVER_MAP_MAX_SIZE = 10; + constructor( private readonly _documents: ExtHostDocuments, private readonly _provider: vscode.HoverProvider, ) { } - async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise { + async provideHover(resource: URI, position: IPosition, context: languages.HoverContext<{ id: number }> | undefined, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); - const value = await this._provider.provideHover(doc, pos, token); + let value: vscode.Hover | null | undefined; + if (context && context.verbosityRequest) { + const previousHoverId = context.verbosityRequest.previousHover.id; + const previousHover = this._hoverMap.get(previousHoverId); + if (!previousHover) { + throw new Error(`Hover with id ${previousHoverId} not found`); + } + const hoverContext: vscode.HoverContext = { verbosityDelta: context.verbosityRequest.verbosityDelta, previousHover }; + value = await this._provider.provideHover(doc, pos, token, hoverContext); + } else { + value = await this._provider.provideHover(doc, pos, token); + } if (!value || isFalsyOrEmpty(value.contents)) { return undefined; } @@ -275,7 +291,24 @@ class HoverAdapter { if (!value.range) { value.range = new Range(pos, pos); } - return typeConvert.Hover.from(value); + const convertedHover: languages.Hover = typeConvert.Hover.from(value); + const id = this._hoverCounter; + // Check if hover map has more than 10 elements and if yes, remove oldest from the map + if (this._hoverMap.size === HoverAdapter.HOVER_MAP_MAX_SIZE) { + const minimumId = Math.min(...this._hoverMap.keys()); + this._hoverMap.delete(minimumId); + } + this._hoverMap.set(id, value); + this._hoverCounter += 1; + const hover: extHostProtocol.HoverWithId = { + ...convertedHover, + id + }; + return hover; + } + + releaseHover(id: number): void { + this._hoverMap.delete(id); } } @@ -468,9 +501,9 @@ class CodeActionAdapter { } else { if (codeActionContext.only) { if (!candidate.kind) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); } else if (!codeActionContext.only.contains(candidate.kind)) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); } } @@ -581,7 +614,7 @@ class DocumentPasteEditProvider { }); const edits = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, { - only: context.only ? new DocumentPasteEditKind(context.only) : undefined, + only: context.only ? new DocumentDropOrPasteEditKind(context.only) : undefined, triggerKind: context.triggerKind, }, token); if (!edits || token.isCancellationRequested) { @@ -759,7 +792,7 @@ class RenameAdapter { private readonly _logService: ILogService ) { } - async provideRenameEdits(resource: URI, position: IPosition, newName: string, token: CancellationToken): Promise { + async provideRenameEdits(resource: URI, position: IPosition, newName: string, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -774,7 +807,7 @@ class RenameAdapter { } catch (err) { const rejectReason = RenameAdapter._asMessage(err); if (rejectReason) { - return { rejectReason, edits: undefined! }; + return { rejectReason, edits: undefined! }; } else { // generic error return Promise.reject(err); @@ -816,7 +849,7 @@ class RenameAdapter { } catch (err) { const rejectReason = RenameAdapter._asMessage(err); if (rejectReason) { - return { rejectReason, range: undefined!, text: undefined! }; + return { rejectReason, range: undefined!, text: undefined! }; } else { return Promise.reject(err); } @@ -836,19 +869,29 @@ class RenameAdapter { class NewSymbolNamesAdapter { + private static languageTriggerKindToVSCodeTriggerKind: Record = { + [languages.NewSymbolNameTriggerKind.Invoke]: NewSymbolNameTriggerKind.Invoke, + [languages.NewSymbolNameTriggerKind.Automatic]: NewSymbolNameTriggerKind.Automatic, + }; + constructor( private readonly _documents: ExtHostDocuments, private readonly _provider: vscode.NewSymbolNamesProvider, private readonly _logService: ILogService ) { } - async provideNewSymbolNames(resource: URI, range: IRange, token: CancellationToken): Promise { + async supportsAutomaticNewSymbolNamesTriggerKind() { + return this._provider.supportsAutomaticTriggerKind; + } + + async provideNewSymbolNames(resource: URI, range: IRange, triggerKind: languages.NewSymbolNameTriggerKind, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const pos = typeConvert.Range.to(range); try { - const value = await this._provider.provideNewSymbolNames(doc, pos, token); + const kind = NewSymbolNamesAdapter.languageTriggerKindToVSCodeTriggerKind[triggerKind]; + const value = await this._provider.provideNewSymbolNames(doc, pos, kind, token); if (!value) { return undefined; } @@ -1244,6 +1287,10 @@ class InlineCompletionAdapterBase { return undefined; } + async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + return undefined; + } + disposeCompletions(pid: number): void { } handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { } @@ -1349,6 +1396,82 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } + override async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + if (!this._provider.provideInlineEdits) { + return undefined; + } + checkProposedApiEnabled(this._extension, 'inlineCompletionsAdditions'); + + const doc = this._documents.getDocument(resource); + const r = typeConvert.Range.to(range); + + const result = await this._provider.provideInlineEdits(doc, r, { + selectedCompletionInfo: + context.selectedSuggestionInfo + ? { + range: typeConvert.Range.to(context.selectedSuggestionInfo.range), + text: context.selectedSuggestionInfo.text + } + : undefined, + triggerKind: this.languageTriggerKindToVSCodeTriggerKind[context.triggerKind], + userPrompt: context.userPrompt, + }, token); + + if (!result) { + // undefined and null are valid results + return undefined; + } + + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + + const normalizedResult = Array.isArray(result) ? result : result.items; + const commands = this._isAdditionsProposedApiEnabled ? Array.isArray(result) ? [] : result.commands || [] : []; + const enableForwardStability = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.enableForwardStability : undefined; + + let disposableStore: DisposableStore | undefined = undefined; + const pid = this._references.createReferenceId({ + dispose() { + disposableStore?.dispose(); + }, + items: normalizedResult + }); + + return { + pid, + items: normalizedResult.map((item, idx) => { + let command: languages.Command | undefined = undefined; + if (item.command) { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + command = this._commands.toInternal(item.command, disposableStore); + } + + const insertText = item.insertText; + return ({ + insertText: typeof insertText === 'string' ? insertText : { snippet: insertText.value }, + filterText: item.filterText, + range: item.range ? typeConvert.Range.from(item.range) : undefined, + command, + idx: idx, + completeBracketPairs: this._isAdditionsProposedApiEnabled ? item.completeBracketPairs : false, + }); + }), + commands: commands.map(c => { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + return this._commands.toInternal(c, disposableStore); + }), + suppressSuggestions: false, + enableForwardStability, + }; + } + override disposeCompletions(pid: number) { const data = this._references.disposeReferenceId(pid); data?.dispose(); @@ -1956,7 +2079,9 @@ class TypeHierarchyAdapter { } } -class DocumentOnDropEditAdapter { +class DocumentDropEditAdapter { + + private readonly _cache = new Cache('DocumentDropEdit'); constructor( private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, @@ -1966,7 +2091,7 @@ class DocumentOnDropEditAdapter { private readonly _extension: IExtensionDescription, ) { } - async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { const doc = this._documents.getDocument(uri); const pos = typeConvert.Position.to(position); const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (id) => { @@ -1978,7 +2103,11 @@ class DocumentOnDropEditAdapter { return undefined; } - return asArray(edits).map((edit): extHostProtocol.IDocumentOnDropEditDto => ({ + const editsArray = asArray(edits); + const cacheId = this._cache.add(editsArray); + + return editsArray.map((edit, i): extHostProtocol.IDocumentDropEditDto => ({ + _cacheId: [cacheId, i], title: edit.title ?? localize('defaultDropLabel', "Drop using '{0}' extension", this._extension.displayName || this._extension.name), kind: edit.kind?.value, yieldTo: edit.yieldTo?.map(x => x.value), @@ -1986,6 +2115,22 @@ class DocumentOnDropEditAdapter { additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit, undefined) : undefined, })); } + + async resolveDropEdit(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: extHostProtocol.IWorkspaceEditDto }> { + const [sessionId, itemId] = id; + const item = this._cache.get(sessionId, itemId); + if (!item || !this._provider.resolveDocumentDropEdit) { + return {}; // this should not happen... + } + + const resolvedItem = (await this._provider.resolveDocumentDropEdit(item, token)) ?? item; + const additionalEdit = resolvedItem.additionalEdit ? typeConvert.WorkspaceEdit.from(resolvedItem.additionalEdit, undefined) : undefined; + return { additionalEdit }; + } + + releaseDropEdits(id: number): any { + this._cache.delete(id); + } } class MappedEditsAdapter { @@ -2036,7 +2181,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter - | DocumentOnDropEditAdapter | MappedEditsAdapter | NewSymbolNamesAdapter | InlineEditAdapter; + | DocumentDropEditAdapter | MappedEditsAdapter | NewSymbolNamesAdapter | InlineEditAdapter; class AdapterData { constructor( @@ -2131,6 +2276,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return ext.displayName || ext.name; } + private static _extId(ext: IExtensionDescription): string { + return ext.identifier.value; + } + // --- outline registerDocumentSymbolProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider, metadata?: vscode.DocumentSymbolProviderMetadata): vscode.Disposable { @@ -2224,8 +2373,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._createDisposable(handle); } - $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { - return this._withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, token), undefined, token); + $provideHover(handle: number, resource: UriComponents, position: IPosition, context: languages.HoverContext<{ id: number }> | undefined, token: CancellationToken,): Promise { + return this._withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, context, token), undefined, token); + } + + $releaseHover(handle: number, id: number): void { + this._withAdapter(handle, HoverAdapter, adapter => Promise.resolve(adapter.releaseHover(id)), undefined, undefined); } // --- debug hover @@ -2316,18 +2469,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, ReferenceAdapter, adapter => adapter.provideReferences(URI.revive(resource), position, context, token), undefined, token); } - // --- quick fix + // --- code actions registerCodeActionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { const store = new DisposableStore(); const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension, this._apiDeprecation), extension); - this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector, extension), { + this._proxy.$registerCodeActionSupport(handle, this._transformDocumentSelector(selector, extension), { providedKinds: metadata?.providedCodeActionKinds?.map(kind => kind.value), documentation: metadata?.documentation?.map(x => ({ kind: x.kind.value, command: this._commands.converter.toInternal(x.command, store), })) - }, ExtHostLanguageFeatures._extLabel(extension), Boolean(provider.resolveCodeAction)); + }, ExtHostLanguageFeatures._extLabel(extension), ExtHostLanguageFeatures._extId(extension), Boolean(provider.resolveCodeAction)); store.add(this._createDisposable(handle)); return store; } @@ -2424,8 +2577,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._createDisposable(handle); } - $provideNewSymbolNames(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { - return this._withAdapter(handle, NewSymbolNamesAdapter, adapter => adapter.provideNewSymbolNames(URI.revive(resource), range, token), undefined, token); + $supportsAutomaticNewSymbolNamesTriggerKind(handle: number): Promise { + return this._withAdapter( + handle, + NewSymbolNamesAdapter, + adapter => adapter.supportsAutomaticNewSymbolNamesTriggerKind(), + false, + undefined + ); + } + + $provideNewSymbolNames(handle: number, resource: UriComponents, range: IRange, triggerKind: languages.NewSymbolNameTriggerKind, token: CancellationToken): Promise { + return this._withAdapter(handle, NewSymbolNamesAdapter, adapter => adapter.provideNewSymbolNames(URI.revive(resource), range, triggerKind, token), undefined, token); } //#region semantic coloring @@ -2498,6 +2661,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined, token); } + $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineEdits(URI.revive(resource), range, context, token), undefined, token); + } + $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void { this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.handleDidShowCompletionItem(pid, idx, updatedInsertText); @@ -2704,23 +2871,34 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF registerDocumentOnDropEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentDropEditProvider, metadata?: vscode.DocumentDropEditProviderMetadata) { const handle = this._nextHandle(); - this._adapter.set(handle, new AdapterData(new DocumentOnDropEditAdapter(this._proxy, this._documents, provider, handle, extension), extension)); + this._adapter.set(handle, new AdapterData(new DocumentDropEditAdapter(this._proxy, this._documents, provider, handle, extension), extension)); - this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), isProposedApiEnabled(extension, 'dropMetadata') ? metadata : undefined); + this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), isProposedApiEnabled(extension, 'documentPaste') && metadata ? { + supportsResolve: !!provider.resolveDocumentDropEdit, + dropMimeTypes: metadata.dropMimeTypes, + } : undefined); return this._createDisposable(handle); } - $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentOnDropEditAdapter, adapter => + $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + return this._withAdapter(handle, DocumentDropEditAdapter, adapter => Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); } + $resolveDropEdit(handle: number, id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<{ additionalEdit?: extHostProtocol.IWorkspaceEditDto }> { + return this._withAdapter(handle, DocumentDropEditAdapter, adapter => adapter.resolveDropEdit(id, token), {}, undefined); + } + + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void { + this._withAdapter(handle, DocumentDropEditAdapter, adapter => Promise.resolve(adapter.releaseDropEdits(cacheId)), undefined, undefined); + } + // --- mapped edits registerMappedEditsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider): vscode.Disposable { const handle = this._addNewAdapter(new MappedEditsAdapter(this._documents, provider), extension); - this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension)); + this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension), extension.displayName ?? extension.name); return this._createDisposable(handle); } diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts new file mode 100644 index 00000000000..f383a4ad265 --- /dev/null +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostLanguageModelToolsShape, IMainContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { IToolData, IToolDelta, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import type * as vscode from 'vscode'; + +export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { + /** A map of tools that were registered in this EH */ + private readonly _registeredTools = new Map(); + private readonly _proxy: MainThreadLanguageModelToolsShape; + + /** A map of all known tools, from other EHs or registered in vscode core */ + private readonly _allTools = new Map(); + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools); + + this._proxy.$getTools().then(tools => { + for (const tool of tools) { + this._allTools.set(tool.name, revive(tool)); + } + }); + } + + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + // Making the round trip here because not all tools were necessarily registered in this EH + const result = await this._proxy.$invokeTool(name, parameters, token); + return typeConvert.LanguageModelToolResult.to(result); + } + + async $acceptToolDelta(delta: IToolDelta): Promise { + if (delta.added) { + this._allTools.set(delta.added.name, delta.added); + } + + if (delta.removed) { + this._allTools.delete(delta.removed); + } + } + + get tools(): vscode.LanguageModelToolDescription[] { + return Array.from(this._allTools.values()) + .map(tool => typeConvert.LanguageModelToolDescription.to(tool)); + } + + async $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + const item = this._registeredTools.get(name); + if (!item) { + throw new Error(`Unknown tool ${name}`); + } + + const extensionResult = await item.tool.invoke(parameters, token); + return typeConvert.LanguageModelToolResult.from(extensionResult); + } + + registerTool(extension: IExtensionDescription, name: string, tool: vscode.LanguageModelTool): IDisposable { + this._registeredTools.set(name, { extension, tool }); + this._proxy.$registerTool(name); + + return toDisposable(() => { + this._registeredTools.delete(name); + this._proxy.$unregisterTool(name); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 8221534013c..b67999350af 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -3,24 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; -import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { LanguageModelError } from 'vs/workbench/api/common/extHostTypes'; -import type * as vscode from 'vscode'; -import { Progress } from 'vs/platform/progress/common/progress'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { CancellationError, SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; -import { CancellationError } from 'vs/base/common/errors'; +import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ILogService } from 'vs/platform/log/common/log'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; +import { IChatMessage, IChatResponseFragment, IChatResponsePart, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import type * as vscode from 'vscode'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -34,13 +37,13 @@ type LanguageModelData = { class LanguageModelResponseStream { - readonly stream = new AsyncIterableSource(); + readonly stream = new AsyncIterableSource(); constructor( readonly option: number, - stream?: AsyncIterableSource + stream?: AsyncIterableSource ) { - this.stream = stream ?? new AsyncIterableSource(); + this.stream = stream ?? new AsyncIterableSource(); } } @@ -49,17 +52,26 @@ class LanguageModelResponse { readonly apiObject: vscode.LanguageModelChatResponse; private readonly _responseStreams = new Map(); - private readonly _defaultStream = new AsyncIterableSource(); + private readonly _defaultStream = new AsyncIterableSource(); private _isDone: boolean = false; - private _isStreaming: boolean = false; constructor() { const that = this; this.apiObject = { // result: promise, - stream: that._defaultStream.asyncIterable, - // streams: AsyncIterable[] // FUTURE responses per N + get stream() { + return that._defaultStream.asyncIterable; + }, + get text() { + return AsyncIterableObject.map(that._defaultStream.asyncIterable, part => { + if (part instanceof extHostTypes.LanguageModelTextPart) { + return part.value; + } else { + return undefined; + } + }).coalesce(); + }, }; } @@ -77,7 +89,6 @@ class LanguageModelResponse { if (this._isDone) { return; } - this._isStreaming = true; let res = this._responseStreams.get(fragment.index); if (!res) { if (this._responseStreams.size === 0) { @@ -88,12 +99,16 @@ class LanguageModelResponse { } this._responseStreams.set(fragment.index, res); } - res.stream.emitOne(fragment.part); + + let out: vscode.LanguageModelChatResponseTextPart | vscode.LanguageModelChatResponseFunctionUsePart; + if (fragment.part.type === 'text') { + out = new extHostTypes.LanguageModelTextPart(fragment.part.value); + } else { + out = new extHostTypes.LanguageModelFunctionUsePart(fragment.part.name, fragment.part.parameters); + } + res.stream.emitOne(out); } - get isStreaming(): boolean { - return this._isStreaming; - } reject(err: Error): void { this._isDone = true; @@ -108,7 +123,6 @@ class LanguageModelResponse { stream.resolve(); } } - } export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { @@ -119,11 +133,11 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { private readonly _proxy: MainThreadLanguageModelsShape; private readonly _onDidChangeModelAccess = new Emitter<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); - private readonly _onDidChangeProviders = new Emitter(); + private readonly _onDidChangeProviders = new Emitter(); readonly onDidChangeProviders = this._onDidChangeProviders.event; private readonly _languageModels = new Map(); - private readonly _allLanguageModelData = new Map(); // these are ALL models, not just the one in this EH + private readonly _allLanguageModelData = new Map }>(); // these are ALL models, not just the one in this EH private readonly _modelAccessList = new ExtensionIdentifierMap(); private readonly _pendingRequest = new Map(); @@ -151,51 +165,113 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { accountLabel: typeof metadata.auth === 'object' ? metadata.auth.label : undefined }; } - this._proxy.$registerLanguageModelProvider(handle, identifier, { + this._proxy.$registerLanguageModelProvider(handle, `${ExtensionIdentifier.toKey(extension.identifier)}/${handle}/${identifier}`, { extension: extension.identifier, - identifier: identifier, - model: metadata.name ?? '', - auth + id: identifier, + vendor: metadata.vendor ?? ExtensionIdentifier.toKey(extension.identifier), + name: metadata.name ?? '', + family: metadata.family ?? '', + version: metadata.version, + maxInputTokens: metadata.maxInputTokens, + maxOutputTokens: metadata.maxOutputTokens, + auth, + targetExtensions: metadata.extensions + }); + + const responseReceivedListener = provider.onDidReceiveLanguageModelResponse2?.(({ extensionId, participant, tokenCount }) => { + this._proxy.$whenLanguageModelChatRequestMade(identifier, new ExtensionIdentifier(extensionId), participant, tokenCount); }); return toDisposable(() => { this._languageModels.delete(handle); this._proxy.$unregisterProvider(handle); + responseReceivedListener?.dispose(); }); } - async $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { + async $startChatRequest(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken): Promise { const data = this._languageModels.get(handle); if (!data) { - return; + throw new Error('Provider not found'); } - const progress = new Progress(async fragment => { + const progress = new Progress(async fragment => { if (token.isCancellationRequested) { this._logService.warn(`[CHAT](${data.extension.value}) CANNOT send progress because the REQUEST IS CANCELLED`); return; } - this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); + + let part: IChatResponsePart | undefined; + if (fragment.part instanceof extHostTypes.LanguageModelFunctionUsePart) { + part = { type: 'function_use', name: fragment.part.name, parameters: fragment.part.parameters }; + } else if (fragment.part instanceof extHostTypes.LanguageModelTextPart) { + part = { type: 'text', value: fragment.part.value }; + } + + if (!part) { + this._logService.warn(`[CHAT](${data.extension.value}) UNKNOWN part ${JSON.stringify(fragment)}`); + return; + } + + this._proxy.$reportResponsePart(requestId, { index: fragment.index, part }); }); - return data.provider.provideLanguageModelResponse2(messages.map(typeConvert.LanguageModelMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); + let p: Promise; + + if (data.provider.provideLanguageModelResponse2) { + + p = Promise.resolve(data.provider.provideLanguageModelResponse2( + messages.map(typeConvert.LanguageModelChatMessage.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + )); + + } else { + + const progress2 = new Progress(async fragment => { + progress.report({ index: fragment.index, part: new extHostTypes.LanguageModelTextPart(fragment.part) }); + }); + + p = Promise.resolve(data.provider.provideLanguageModelResponse( + messages.map(typeConvert.LanguageModelChatMessage.to), + options?.modelOptions ?? {}, + ExtensionIdentifier.toKey(from), + progress2, + token + )); + } + + p.then(() => { + this._proxy.$reportResponseDone(requestId, undefined); + }, err => { + this._proxy.$reportResponseDone(requestId, transformErrorForSerialization(err)); + }); } + //#region --- token counting + + $provideTokenLength(handle: number, value: string, token: CancellationToken): Promise { + const data = this._languageModels.get(handle); + if (!data) { + return Promise.resolve(0); + } + return Promise.resolve(data.provider.provideTokenCount(value, token)); + } + + //#region --- making request - $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[] | undefined; removed?: string[] | undefined }): void { - const added: string[] = []; - const removed: string[] = []; + $acceptChatModelMetadata(data: { added?: { identifier: string; metadata: ILanguageModelChatMetadata }[] | undefined; removed?: string[] | undefined }): void { if (data.added) { - for (const metadata of data.added) { - this._allLanguageModelData.set(metadata.identifier, metadata); - added.push(metadata.model); + for (const { identifier, metadata } of data.added) { + this._allLanguageModelData.set(identifier, { metadata, apiObjects: new ExtensionIdentifierMap() }); } } if (data.removed) { for (const id of data.removed) { // clean up this._allLanguageModelData.delete(id); - removed.push(id); // cancel pending requests for this model for (const [key, value] of this._pendingRequest) { @@ -207,17 +283,215 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - this._onDidChangeProviders.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed) - })); - // TODO@jrieken@TylerLeonhardt - this is a temporary hack to populate the auth providers - data.added?.forEach(this._fakeAuthPopulate, this); + data.added?.forEach(added => this._fakeAuthPopulate(added.metadata)); + + this._onDidChangeProviders.fire(undefined); } - getLanguageModelIds(): string[] { - return Array.from(this._allLanguageModelData.keys()); + async selectLanguageModels(extension: IExtensionDescription, selector: vscode.LanguageModelChatSelector) { + + // this triggers extension activation + const models = await this._proxy.$selectChatModels({ ...selector, extension: extension.identifier }); + + const result: vscode.LanguageModelChat[] = []; + const that = this; + for (const identifier of models) { + const data = this._allLanguageModelData.get(identifier); + if (!data) { + // model gone? is this an error on us? + continue; + } + + // make sure auth information is correct + if (this._isUsingAuth(extension.identifier, data.metadata)) { + await this._fakeAuthPopulate(data.metadata); + } + + let apiObject = data.apiObjects.get(extension.identifier); + + if (!apiObject) { + apiObject = { + id: identifier, + vendor: data.metadata.vendor, + family: data.metadata.family, + version: data.metadata.version, + name: data.metadata.name, + maxInputTokens: data.metadata.maxInputTokens, + countTokens(text, token) { + if (!that._allLanguageModelData.has(identifier)) { + throw extHostTypes.LanguageModelError.NotFound(identifier); + } + return that._computeTokenLength(identifier, text, token ?? CancellationToken.None); + }, + sendRequest(messages, options, token) { + if (!that._allLanguageModelData.has(identifier)) { + throw extHostTypes.LanguageModelError.NotFound(identifier); + } + return that._sendChatRequest(extension, identifier, messages, options ?? {}, token ?? CancellationToken.None); + } + }; + + Object.freeze(apiObject); + data.apiObjects.set(extension.identifier, apiObject); + } + + result.push(apiObject); + } + + return result; + } + + private async _sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { + + const internalMessages: IChatMessage[] = this._convertMessages(extension, messages); + + const from = extension.identifier; + const metadata = this._allLanguageModelData.get(languageModelId)?.metadata; + + if (!metadata || !this._allLanguageModelData.has(languageModelId)) { + throw extHostTypes.LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); + } + + if (this._isUsingAuth(from, metadata)) { + const success = await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, options.justification, false); + + if (!success || !this._modelAccessList.get(from)?.has(metadata.extension)) { + throw extHostTypes.LanguageModelError.NoPermissions(`Language model '${languageModelId}' cannot be used by '${from.value}'.`); + } + } + + try { + const requestId = (Math.random() * 1e6) | 0; + const res = new LanguageModelResponse(); + this._pendingRequest.set(requestId, { languageModelId, res }); + + try { + await this._proxy.$tryStartChatRequest(from, languageModelId, requestId, internalMessages, options, token); + + } catch (error) { + // error'ing here means that the request could NOT be started/made, e.g. wrong model, no access, etc, but + // later the response can fail as well. Those failures are communicated via the stream-object + this._pendingRequest.delete(requestId); + throw error; + } + + return res.apiObject; + + } catch (error) { + if (error.name === extHostTypes.LanguageModelError.name) { + throw error; + } + throw new extHostTypes.LanguageModelError( + `Language model '${languageModelId}' errored: ${toErrorMessage(error)}`, + 'Unknown', + error + ); + } + } + + private _convertMessages(extension: IExtensionDescription, messages: vscode.LanguageModelChatMessage[]) { + const internalMessages: IChatMessage[] = []; + for (const message of messages) { + if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { + checkProposedApiEnabled(extension, 'languageModelSystem'); + } + if (message.content2 instanceof extHostTypes.LanguageModelFunctionResultPart) { + checkProposedApiEnabled(extension, 'lmTools'); + } + internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); + } + return internalMessages; + } + + async $acceptResponsePart(requestId: number, chunk: IChatResponseFragment): Promise { + const data = this._pendingRequest.get(requestId); + if (data) { + data.res.handleFragment(chunk); + } + } + + async $acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise { + const data = this._pendingRequest.get(requestId); + if (!data) { + return; + } + this._pendingRequest.delete(requestId); + if (error) { + // we error the stream because that's the only way to signal + // that the request has failed + data.res.reject(transformErrorFromSerialization(error)); + } else { + data.res.resolve(); + } + } + + // BIG HACK: Using AuthenticationProviders to check access to Language Models + private async _getAuthAccess(from: IExtensionDescription, to: { identifier: ExtensionIdentifier; displayName: string }, justification: string | undefined, silent: boolean | undefined): Promise { + // This needs to be done in both MainThread & ExtHost ChatProvider + const providerId = INTERNAL_AUTH_PROVIDER_PREFIX + to.identifier.value; + const session = await this._extHostAuthentication.getSession(from, providerId, [], { silent: true }); + + if (session) { + this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); + return true; + } + + if (silent) { + return false; + } + + try { + const detail = justification + ? localize('chatAccessWithJustification', "Justification: {1}", to.displayName, justification) + : undefined; + await this._extHostAuthentication.getSession(from, providerId, [], { forceNewSession: { detail } }); + this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); + return true; + + } catch (err) { + // ignore + return false; + } + } + + private _isUsingAuth(from: ExtensionIdentifier, toMetadata: ILanguageModelChatMetadata): toMetadata is ILanguageModelChatMetadata & { auth: NonNullable } { + // If the 'to' extension uses an auth check + return !!toMetadata.auth + // And we're asking from a different extension + && !ExtensionIdentifier.equals(toMetadata.extension, from); + } + + private async _fakeAuthPopulate(metadata: ILanguageModelChatMetadata): Promise { + + if (!metadata.auth) { + return; + } + + for (const from of this._languageAccessInformationExtensions) { + try { + await this._getAuthAccess(from, { identifier: metadata.extension, displayName: '' }, undefined, true); + } catch (err) { + this._logService.error('Fake Auth request failed'); + this._logService.error(err); + } + } + } + + private async _computeTokenLength(languageModelId: string, value: string | vscode.LanguageModelChatMessage, token: vscode.CancellationToken): Promise { + + const data = this._allLanguageModelData.get(languageModelId); + if (!data) { + throw extHostTypes.LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); + } + + const local = Iterable.find(this._languageModels.values(), candidate => candidate.languageModelId === languageModelId); + if (local) { + // stay inside the EH + return local.provider.provideTokenCount(value, token); + } + + return this._proxy.$countTokens(languageModelId, (typeof value === 'string' ? value : typeConvert.LanguageModelChatMessage.from(value)), token); } $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { @@ -239,116 +513,6 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - async sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { - - const from = extension.identifier; - const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, options.justification); - - if (!metadata || !this._allLanguageModelData.has(languageModelId)) { - throw LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); - } - - if (this._isUsingAuth(from, metadata)) { - const success = await this._getAuthAccess(extension, { identifier: metadata.extension, displayName: metadata.auth.providerLabel }, options.justification, options.silent); - - if (!success || !this._modelAccessList.get(from)?.has(metadata.extension)) { - throw LanguageModelError.NoPermissions(`Language model '${languageModelId}' cannot be used by '${from.value}'.`); - } - } - - const requestId = (Math.random() * 1e6) | 0; - const requestPromise = this._proxy.$fetchResponse(from, languageModelId, requestId, messages.map(typeConvert.LanguageModelMessage.from), options.modelOptions ?? {}, token); - - const barrier = new Barrier(); - - const res = new LanguageModelResponse(); - this._pendingRequest.set(requestId, { languageModelId, res }); - - let error: Error | undefined; - - requestPromise.catch(err => { - if (barrier.isOpen()) { - // we received an error while streaming. this means we need to reject the "stream" - // because we have already returned the request object - res.reject(err); - } else { - error = err; - } - }).finally(() => { - this._pendingRequest.delete(requestId); - res.resolve(); - barrier.open(); - }); - - await barrier.wait(); - - if (error) { - throw new LanguageModelError( - `Language model '${languageModelId}' errored, check cause for more details`, - 'Unknown', - error - ); - } - - return res.apiObject; - } - - async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise { - const data = this._pendingRequest.get(requestId);//.report(chunk); - if (data) { - data.res.handleFragment(chunk); - } - } - - // BIG HACK: Using AuthenticationProviders to check access to Language Models - private async _getAuthAccess(from: IExtensionDescription, to: { identifier: ExtensionIdentifier; displayName: string }, justification: string | undefined, silent: boolean | undefined): Promise { - // This needs to be done in both MainThread & ExtHost ChatProvider - const providerId = INTERNAL_AUTH_PROVIDER_PREFIX + to.identifier.value; - const session = await this._extHostAuthentication.getSession(from, providerId, [], { silent: true }); - - if (session) { - this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); - return true; - } - - if (silent) { - return false; - } - - try { - const detail = justification - ? localize('chatAccessWithJustification', "To allow access to the language models provided by {0}. Justification:\n\n{1}", to.displayName, justification) - : localize('chatAccess', "To allow access to the language models provided by {0}", to.displayName); - await this._extHostAuthentication.getSession(from, providerId, [], { forceNewSession: { detail } }); - this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); - return true; - - } catch (err) { - // ignore - return false; - } - } - - private _isUsingAuth(from: ExtensionIdentifier, toMetadata: ILanguageModelChatMetadata): toMetadata is ILanguageModelChatMetadata & { auth: NonNullable } { - // If the 'to' extension uses an auth check - return !!toMetadata.auth - // And we're asking from a different extension - && !ExtensionIdentifier.equals(toMetadata.extension, from); - } - - private async _fakeAuthPopulate(metadata: ILanguageModelChatMetadata): Promise { - - for (const from of this._languageAccessInformationExtensions) { - try { - await this._getAuthAccess(from, { identifier: metadata.extension, displayName: '' }, undefined, true); - } catch (err) { - this._logService.error('Fake Auth request failed'); - this._logService.error(err); - } - } - - } - private readonly _languageAccessInformationExtensions = new Set>(); createLanguageModelAccessInformation(from: Readonly): vscode.LanguageModelAccessInformation { @@ -363,13 +527,22 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { get onDidChange() { return Event.any(_onDidChangeAccess, _onDidAddRemove); }, - canSendRequest(languageModelId: string): boolean | undefined { + canSendRequest(chat: vscode.LanguageModelChat): boolean | undefined { - const data = that._allLanguageModelData.get(languageModelId); - if (!data) { + let metadata: ILanguageModelChatMetadata | undefined; + + out: for (const [_, value] of that._allLanguageModelData) { + for (const candidate of value.apiObjects.values()) { + if (candidate === chat) { + metadata = value.metadata; + break out; + } + } + } + if (!metadata) { return undefined; } - if (!that._isUsingAuth(from.identifier, data)) { + if (!that._isUsingAuth(from.identifier, metadata)) { return true; } @@ -377,7 +550,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (!list) { return undefined; } - return list.has(data.extension); + return list.has(metadata.extension); } }; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 75dc5a402ff..5b707fc865e 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -36,7 +36,8 @@ import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; import { INotebookCellMatchNoModel, INotebookFileMatchNoModel, IRawClosedNotebookFileMatch, genericCellMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; -import { globMatchesResource } from 'vs/workbench/services/editor/common/editorResolverService'; +import { globMatchesResource, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostNotebookController implements ExtHostNotebookShape { private static _notebookStatusBarItemProviderHandlePool: number = 0; @@ -78,7 +79,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private _textDocuments: ExtHostDocuments, private _extHostFileSystem: IExtHostConsumerFileSystem, - private _extHostSearch: IExtHostSearch + private _extHostSearch: IExtHostSearch, + private _logService: ILogService ) { this._notebookProxy = mainContext.getProxy(MainContext.MainThreadNotebook); this._notebookDocumentsProxy = mainContext.getProxy(MainContext.MainThreadNotebookDocuments); @@ -161,7 +163,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { providerDisplayName: extension.displayName || extension.name, displayName: registration.displayName, filenamePattern: viewOptionsFilenamePattern, - exclusive: registration.exclusive || false + priority: registration.exclusive ? RegisteredEditorPriority.exclusive : undefined }; } @@ -314,6 +316,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { async $saveNotebook(handle: number, uriComponents: UriComponents, versionId: number, options: files.IWriteFileOptions, token: CancellationToken): Promise { const uri = URI.revive(uriComponents); const serializer = this._notebookSerializer.get(handle); + this.trace(`enter saveNotebook(versionId: ${versionId}, ${uri.toString()})`); + if (!serializer) { throw new Error('NO serializer found'); } @@ -331,14 +335,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { throw new files.FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this._resourceForError(uri)), files.FileOperationResult.FILE_PERMISSION_DENIED); } - // validate write - await this._validateWriteFile(uri, options); - const data: vscode.NotebookData = { metadata: filter(document.apiNotebook.metadata, key => !(serializer.options?.transientDocumentMetadata ?? {})[key]), cells: [], }; + // this data must be retrieved before any async calls to ensure the data is for the correct version for (const cell of document.apiNotebook.getCells()) { const cellData = new extHostTypes.NotebookCellData( cell.kind, @@ -354,8 +356,21 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { data.cells.push(cellData); } + // validate write + await this._validateWriteFile(uri, options); + + if (token.isCancellationRequested) { + throw new Error('canceled'); + } const bytes = await serializer.serializer.serializeNotebook(data, token); + if (token.isCancellationRequested) { + throw new Error('canceled'); + } + + // Don't accept any cancellation beyond this point, we need to report the result of the file write + this.trace(`serialized versionId: ${versionId} ${uri.toString()}`); await this._extHostFileSystem.value.writeFile(uri, bytes); + this.trace(`Finished write versionId: ${versionId} ${uri.toString()}`); const providerExtUri = this._extHostFileSystem.getFileSystemProviderExtUri(uri.scheme); const stat = await this._extHostFileSystem.value.stat(uri); @@ -373,6 +388,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { children: undefined }; + this.trace(`exit saveNotebook(versionId: ${versionId}, ${uri.toString()})`); return fileStats; } @@ -716,4 +732,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { extHostCommands.registerApiCommand(commandDataToNotebook); extHostCommands.registerApiCommand(commandNotebookToData); } + + private trace(msg: string): void { + this._logService.trace(`[Extension Host Notebook] ${msg}`); + } } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 8f74a0a4b69..382fd39c30a 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -211,6 +211,9 @@ export class ExtHostNotebookDocument { }, save() { return that._save(); + }, + [Symbol.for('debug.description')]() { + return `NotebookDocument(${this.uri.toString()})`; } }; this._notebook = Object.freeze(apiObject); diff --git a/src/vs/workbench/api/common/extHostNotebookDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostNotebookDocumentSaveParticipant.ts index 1e6a706a305..0f031e2f76b 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocumentSaveParticipant.ts @@ -13,6 +13,7 @@ import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebo import { TextDocumentSaveReason, WorkspaceEdit as WorksapceEditConverter } from 'vs/workbench/api/common/extHostTypeConverters'; import { WorkspaceEdit } from 'vs/workbench/api/common/extHostTypes'; import { SaveReason } from 'vs/workbench/common/editor'; +import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { NotebookDocumentWillSaveEvent } from 'vscode'; interface IExtensionListener { @@ -90,6 +91,6 @@ export class ExtHostNotebookDocumentSaveParticipant implements ExtHostNotebookDo dto.edits = dto.edits.concat(edits); } - return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto); + return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers(dto)); } } diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index 8472fc1006d..4aec54c2651 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -71,6 +71,9 @@ export class ExtHostNotebookEditor { get viewColumn() { return that._viewColumn; }, + [Symbol.for('debug.description')]() { + return `NotebookEditor(${this.notebook.uri.toString()})`; + } }; ExtHostNotebookEditor.apiEditorsToExtHost.set(this._editor, this); diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 49dcbf40085..ecb6b4a7db3 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -433,6 +433,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { if (obj.controller.interruptHandler) { // If we're interrupting all cells, we also need to cancel the notebook level execution. const items = this._activeNotebookExecutions.get(document.uri); + this._activeNotebookExecutions.delete(document.uri); if (handles.length && Array.isArray(items) && items.length) { items.forEach(d => d.dispose()); } diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index edc8a951ffc..3a9075f953b 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -13,7 +13,7 @@ import { ExtHostQuickOpenShape, IMainContext, MainContext, TransferQuickInput, T import { URI } from 'vs/base/common/uri'; import { ThemeIcon, QuickInputButtons, QuickPickItemKind, InputBoxValidationSeverity } from 'vs/workbench/api/common/extHostTypes'; import { isCancellationError } from 'vs/base/common/errors'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; import Severity from 'vs/base/common/severity'; import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; @@ -64,7 +64,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx // clear state from last invocation this._onDidSelectItem = undefined; - const itemsPromise = >Promise.resolve(itemsOrItemsPromise); + const itemsPromise = Promise.resolve(itemsOrItemsPromise); const instance = ++this._instances; @@ -301,7 +301,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._onDidChangeValueEmitter ]; - constructor(protected _extensionId: ExtensionIdentifier, private _onDidDispose: () => void) { + constructor(protected _extension: IExtensionDescription, private _onDidDispose: () => void) { } get title() { @@ -385,6 +385,10 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } set buttons(buttons: QuickInputButton[]) { + const allowedButtonLocation = isProposedApiEnabled(this._extension, 'quickInputButtonLocation'); + if (!allowedButtonLocation && buttons.some(button => button.location)) { + console.warn(`Extension '${this._extension.identifier.value}' uses a button location which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.identifier.value}`); + } this._buttons = buttons.slice(); this._handlesToButtons.clear(); buttons.forEach((button, i) => { @@ -397,6 +401,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx ...getIconPathOrClass(button.iconPath), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : i, + location: allowedButtonLocation ? button.location : undefined }; }) }); @@ -546,8 +551,8 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private readonly _onDidChangeSelectionEmitter = new Emitter(); private readonly _onDidTriggerItemButtonEmitter = new Emitter>(); - constructor(private extension: IExtensionDescription, onDispose: () => void) { - super(extension.identifier, onDispose); + constructor(extension: IExtensionDescription, onDispose: () => void) { + super(extension, onDispose); this._disposables.push( this._onDidChangeActiveEmitter, this._onDidChangeSelectionEmitter, @@ -569,7 +574,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._itemsToHandles.set(item, i); }); - const allowedTooltips = isProposedApiEnabled(this.extension, 'quickPickItemTooltip'); + const allowedTooltips = isProposedApiEnabled(this._extension, 'quickPickItemTooltip'); const pickItems: TransferQuickPickItemOrSeparator[] = []; for (let handle = 0; handle < items.length; handle++) { @@ -578,7 +583,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx pickItems.push({ type: 'separator', label: item.label }); } else { if (item.tooltip && !allowedTooltips) { - console.warn(`Extension '${this.extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this.extension.identifier.value}`); + console.warn(`Extension '${this._extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this._extension.identifier.value}`); } const icon = (item.iconPath) ? getIconPathOrClass(item.iconPath) : undefined; @@ -712,7 +717,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _validationMessage: string | InputBoxValidationMessage | undefined; constructor(extension: IExtensionDescription, onDispose: () => void) { - super(extension.identifier, onDispose); + super(extension, onDispose); this.update({ type: 'inputBox' }); } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index ee2be0b1bcc..b07e8f498b2 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -58,19 +58,26 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor } } -function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { - if (!historyItem.icon) { +function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { + if (!icon) { return undefined; - } else if (URI.isUri(historyItem.icon)) { - return historyItem.icon; - } else if (ThemeIcon.isThemeIcon(historyItem.icon)) { - return historyItem.icon; + } else if (URI.isUri(icon)) { + return icon; + } else if (ThemeIcon.isThemeIcon(icon)) { + return icon; } else { - const icon = historyItem.icon as { light: URI; dark: URI }; - return { light: icon.light, dark: icon.dark }; + const iconDto = icon as { light: URI; dark: URI }; + return { light: iconDto.light, dark: iconDto.dark }; } } +function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { + const icon = getHistoryItemIconDto(historyItem.icon); + const labels = historyItem.labels?.map(l => ({ title: l.title, icon: getHistoryItemIconDto(l.icon) })); + + return { ...historyItem, icon, labels }; +} + function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { if (!a.iconPath && !b.iconPath) { return 0; @@ -559,7 +566,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } private _historyProvider: vscode.SourceControlHistoryProvider | undefined; - private _historyProviderDisposable = new MutableDisposable(); + private readonly _historyProviderDisposable = new MutableDisposable(); private _historyProviderCurrentHistoryItemGroup: vscode.SourceControlHistoryItemGroup | undefined; get historyProvider(): vscode.SourceControlHistoryProvider | undefined { @@ -598,7 +605,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this.#proxy.$updateSourceControl(this.handle, { commitTemplate }); } - private _acceptInputDisposables = new MutableDisposable(); + private readonly _acceptInputDisposables = new MutableDisposable(); private _acceptInputCommand: vscode.Command | undefined = undefined; get acceptInputCommand(): vscode.Command | undefined { @@ -614,7 +621,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this.#proxy.$updateSourceControl(this.handle, { acceptInputCommand: internal }); } - private _actionButtonDisposables = new MutableDisposable(); + private readonly _actionButtonDisposables = new MutableDisposable(); private _actionButton: vscode.SourceControlActionButton | undefined; get actionButton(): vscode.SourceControlActionButton | undefined { checkProposedApiEnabled(this._extension, 'scmActionButton'); @@ -639,7 +646,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } - private _statusBarDisposables = new MutableDisposable(); + private readonly _statusBarDisposables = new MutableDisposable(); private _statusBarCommands: vscode.Command[] | undefined = undefined; get statusBarCommands(): vscode.Command[] | undefined { @@ -965,11 +972,23 @@ export class ExtHostSCM implements ExtHostSCMShape { return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2, token) ?? undefined; } + async $resolveHistoryItemGroupCommonAncestor2(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + return await historyProvider?.resolveHistoryItemGroupCommonAncestor2(historyItemGroupIds, token) ?? undefined; + } + async $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token); - return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined; + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; + } + + async $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const historyItems = await historyProvider?.provideHistoryItems2(options, token); + + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; } async $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { @@ -979,7 +998,7 @@ export class ExtHostSCM implements ExtHostSCMShape { } const historyItem = await historyProvider.provideHistoryItemSummary(historyItemId, historyItemParentId, token); - return historyItem ? { ...historyItem, icon: getHistoryItemIconDto(historyItem) } : undefined; + return historyItem ? toSCMHistoryItemDto(historyItem) : undefined; } async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index c2e0b93f7b9..f7da534aab8 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -25,7 +25,7 @@ export interface IExtHostSearch extends ExtHostSearchShape { export const IExtHostSearch = createDecorator('IExtHostSearch'); -export class ExtHostSearch implements ExtHostSearchShape { +export class ExtHostSearch implements IExtHostSearch { protected readonly _proxy: MainThreadSearchShape = this.extHostRpc.getProxy(MainContext.MainThreadSearch); protected _handlePool: number = 0; @@ -124,7 +124,7 @@ export class ExtHostSearch implements ExtHostSearchShape { $provideTextSearchResults(handle: number, session: number, rawQuery: IRawTextQuery, token: vscode.CancellationToken): Promise { const provider = this._textSearchProvider.get(handle); if (!provider || !provider.provideTextSearchResults) { - throw new Error(`2 Unknown provider ${handle}`); + throw new Error(`Unknown Text Search Provider ${handle}`); } const query = reviveQuery(rawQuery); @@ -135,7 +135,7 @@ export class ExtHostSearch implements ExtHostSearchShape { $provideAITextSearchResults(handle: number, session: number, rawQuery: IRawAITextQuery, token: vscode.CancellationToken): Promise { const provider = this._aiTextSearchProvider.get(handle); if (!provider || !provider.provideAITextSearchResults) { - throw new Error(`1 Unknown provider ${handle}`); + throw new Error(`Unknown AI Text Search Provider ${handle}`); } const query = reviveQuery(rawQuery); diff --git a/src/vs/workbench/api/common/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts index d1af02ed1a2..13fb3293a35 100644 --- a/src/vs/workbench/api/common/extHostSecrets.ts +++ b/src/vs/workbench/api/common/extHostSecrets.ts @@ -9,26 +9,30 @@ import type * as vscode from 'vscode'; import { ExtHostSecretState } from 'vs/workbench/api/common/extHostSecretState'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export class ExtensionSecrets implements vscode.SecretStorage { protected readonly _id: string; readonly #secretState: ExtHostSecretState; - private _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - + readonly onDidChange: Event; + readonly disposables = new DisposableStore(); constructor(extensionDescription: IExtensionDescription, secretState: ExtHostSecretState) { this._id = ExtensionIdentifier.toKey(extensionDescription.identifier); this.#secretState = secretState; - this.#secretState.onDidChangePassword(e => { - if (e.extensionId === this._id) { - this._onDidChange.fire({ key: e.key }); - } - }); + this.onDidChange = Event.map( + Event.filter(this.#secretState.onDidChangePassword, e => e.extensionId === this._id), + e => ({ key: e.key }), + this.disposables + ); + } + + dispose() { + this.disposables.dispose(); } get(key: string): Promise { diff --git a/src/vs/workbench/api/common/extHostSpeech.ts b/src/vs/workbench/api/common/extHostSpeech.ts index 9093f63e3ab..198eaee26ad 100644 --- a/src/vs/workbench/api/common/extHostSpeech.ts +++ b/src/vs/workbench/api/common/extHostSpeech.ts @@ -17,6 +17,7 @@ export class ExtHostSpeech implements ExtHostSpeechShape { private readonly providers = new Map(); private readonly sessions = new Map(); + private readonly synthesizers = new Map(); constructor( mainContext: IMainContext @@ -35,7 +36,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const speechToTextSession = disposables.add(provider.provideSpeechToTextSession(cts.token, language ? { language } : undefined)); + const speechToTextSession = await provider.provideSpeechToTextSession(cts.token, language ? { language } : undefined); + if (!speechToTextSession) { + return; + } + disposables.add(speechToTextSession.onDidChange(e => { if (cts.token.isCancellationRequested) { return; @@ -52,6 +57,45 @@ export class ExtHostSpeech implements ExtHostSpeechShape { this.sessions.delete(session); } + async $createTextToSpeechSession(handle: number, session: number, language?: string): Promise { + const provider = this.providers.get(handle); + if (!provider) { + return; + } + + const disposables = new DisposableStore(); + + const cts = new CancellationTokenSource(); + this.sessions.set(session, cts); + + const textToSpeech = await provider.provideTextToSpeechSession(cts.token, language ? { language } : undefined); + if (!textToSpeech) { + return; + } + + this.synthesizers.set(session, textToSpeech); + + disposables.add(textToSpeech.onDidChange(e => { + if (cts.token.isCancellationRequested) { + return; + } + + this.proxy.$emitTextToSpeechEvent(session, e); + })); + + disposables.add(cts.token.onCancellationRequested(() => disposables.dispose())); + } + + async $synthesizeSpeech(session: number, text: string): Promise { + this.synthesizers.get(session)?.synthesize(text); + } + + async $cancelTextToSpeechSession(session: number): Promise { + this.sessions.get(session)?.dispose(true); + this.sessions.delete(session); + this.synthesizers.delete(session); + } + async $createKeywordRecognitionSession(handle: number, session: number): Promise { const provider = this.providers.get(handle); if (!provider) { @@ -63,7 +107,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const keywordRecognitionSession = disposables.add(provider.provideKeywordRecognitionSession(cts.token)); + const keywordRecognitionSession = await provider.provideKeywordRecognitionSession(cts.token); + if (!keywordRecognitionSession) { + return; + } + disposables.add(keywordRecognitionSession.onDidChange(e => { if (cts.token.isCancellationRequested) { return; diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 7e8db34a599..982cdc28c20 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -46,6 +46,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _name?: string; private _color?: string | ThemeColor; private _backgroundColor?: ThemeColor; + // eslint-disable-next-line local/code-no-potentially-unsafe-disposables private _latestCommandRegistration?: DisposableStore; private readonly _staleCommandRegistrations = new DisposableStore(); private _command?: { diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 896160e4d92..64a12869610 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -105,6 +105,7 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap commonProperties['common.vscodemachineid'] = this.initData.telemetryInfo.machineId; commonProperties['common.vscodesessionid'] = this.initData.telemetryInfo.sessionId; commonProperties['common.sqmid'] = this.initData.telemetryInfo.sqmId; + commonProperties['common.devDeviceId'] = this.initData.telemetryInfo.devDeviceId; commonProperties['common.vscodeversion'] = this.initData.version; commonProperties['common.isnewappinstall'] = isNewAppInstall(this.initData.telemetryInfo.firstSessionDate); commonProperties['common.product'] = this.initData.environment.appHost; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index fb4f5520c02..9ce31e7147c 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -393,7 +393,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I protected _environmentVariableCollections: Map = new Map(); private _defaultProfile: ITerminalProfile | undefined; private _defaultAutomationProfile: ITerminalProfile | undefined; - private _lastQuickFixCommands: MutableDisposable = this._register(new MutableDisposable()); + private readonly _lastQuickFixCommands: MutableDisposable = this._register(new MutableDisposable()); private readonly _bufferer: TerminalDataBufferer; private readonly _linkProviders: Set = new Set(); @@ -921,7 +921,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection { let collection = this._environmentVariableCollections.get(extension.identifier.value); if (!collection) { - collection = new UnifiedEnvironmentVariableCollection(); + collection = this._register(new UnifiedEnvironmentVariableCollection()); this._setEnvironmentVariableCollection(extension.identifier.value, collection); } return collection.getScopedEnvironmentVariableCollection(undefined); @@ -936,7 +936,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void { collections.forEach(entry => { const extensionIdentifier = entry[0]; - const collection = new UnifiedEnvironmentVariableCollection(entry[1]); + const collection = this._register(new UnifiedEnvironmentVariableCollection(entry[1])); this._setEnvironmentVariableCollection(extensionIdentifier, collection); }); } @@ -952,20 +952,20 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I private _setEnvironmentVariableCollection(extensionIdentifier: string, collection: UnifiedEnvironmentVariableCollection): void { this._environmentVariableCollections.set(extensionIdentifier, collection); - collection.onDidChangeCollection(() => { + this._register(collection.onDidChangeCollection(() => { // When any collection value changes send this immediately, this is done to ensure // following calls to createTerminal will be created with the new environment. It will // result in more noise by sending multiple updates when called but collections are // expected to be small. this._syncEnvironmentVariableCollection(extensionIdentifier, collection); - }); + })); } } /** * Unified environment variable collection carrying information for all scopes, for a specific extension. */ -class UnifiedEnvironmentVariableCollection { +class UnifiedEnvironmentVariableCollection extends Disposable { readonly map: Map = new Map(); private readonly scopedCollections: Map = new Map(); readonly descriptionMap: Map = new Map(); @@ -983,6 +983,7 @@ class UnifiedEnvironmentVariableCollection { constructor( serialized?: ISerializableEnvironmentVariableCollection ) { + super(); this.map = new Map(serialized); } @@ -992,7 +993,7 @@ class UnifiedEnvironmentVariableCollection { if (!scopedCollection) { scopedCollection = new ScopedEnvironmentVariableCollection(this, scope); this.scopedCollections.set(scopedCollectionKey, scopedCollection); - scopedCollection.onDidChangeCollection(() => this._onDidChangeCollection.fire()); + this._register(scopedCollection.onDidChangeCollection(() => this._onDidChangeCollection.fire())); } return scopedCollection; } diff --git a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts index 0e2a6358f13..06417a27963 100644 --- a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts +++ b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts @@ -4,21 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; +import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { MainContext, type ExtHostTerminalShellIntegrationShape, type MainThreadTerminalShellIntegrationShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { Emitter, type Event } from 'vs/base/common/event'; -import { URI } from 'vs/base/common/uri'; +import { URI, type UriComponents } from 'vs/base/common/uri'; import { AsyncIterableObject, Barrier, type AsyncIterableEmitter } from 'vs/base/common/async'; export interface IExtHostTerminalShellIntegration extends ExtHostTerminalShellIntegrationShape { readonly _serviceBrand: undefined; readonly onDidChangeTerminalShellIntegration: Event; - readonly onDidStartTerminalShellExecution: Event; - readonly onDidEndTerminalShellExecution: Event; + readonly onDidStartTerminalShellExecution: Event; + readonly onDidEndTerminalShellExecution: Event; } export const IExtHostTerminalShellIntegration = createDecorator('IExtHostTerminalShellIntegration'); @@ -32,9 +33,9 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH protected readonly _onDidChangeTerminalShellIntegration = new Emitter(); readonly onDidChangeTerminalShellIntegration = this._onDidChangeTerminalShellIntegration.event; - protected readonly _onDidStartTerminalShellExecution = new Emitter(); + protected readonly _onDidStartTerminalShellExecution = new Emitter(); readonly onDidStartTerminalShellExecution = this._onDidStartTerminalShellExecution.event; - protected readonly _onDidEndTerminalShellExecution = new Emitter(); + protected readonly _onDidEndTerminalShellExecution = new Emitter(); readonly onDidEndTerminalShellExecution = this._onDidEndTerminalShellExecution.event; constructor( @@ -61,12 +62,12 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH // console.log('*** onDidStartTerminalShellExecution', e); // // new Promise(r => { // // (async () => { - // // for await (const d of e.createDataStream()) { + // // for await (const d of e.execution.read()) { // // console.log('data2', d); // // } // // })(); // // }); - // for await (const d of e.createDataStream()) { + // for await (const d of e.execution.read()) { // console.log('data', d); // } // }); @@ -74,7 +75,9 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH // console.log('*** onDidEndTerminalShellExecution', e); // }); // setTimeout(() => { + // console.log('before executeCommand(\"echo hello\")'); // Array.from(this._activeShellIntegrations.values())[0].value.executeCommand('echo hello'); + // console.log('after executeCommand(\"echo hello\")'); // }, 4000); } @@ -91,7 +94,7 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH this._activeShellIntegrations.set(instanceId, shellIntegration); shellIntegration.store.add(terminal.onWillDispose(() => this._activeShellIntegrations.get(instanceId)?.dispose())); shellIntegration.store.add(shellIntegration.onDidRequestShellExecution(commandLine => this._proxy.$executeCommand(instanceId, commandLine))); - shellIntegration.store.add(shellIntegration.onDidRequestEndExecution(e => this._onDidEndTerminalShellExecution.fire(e.value))); + shellIntegration.store.add(shellIntegration.onDidRequestEndExecution(e => this._onDidEndTerminalShellExecution.fire(e))); shellIntegration.store.add(shellIntegration.onDidRequestChangeShellIntegration(e => this._onDidChangeTerminalShellIntegration.fire(e))); terminal.shellIntegration = shellIntegration.value; } @@ -101,16 +104,26 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH }); } - public $shellExecutionStart(instanceId: number, commandLine: string, cwd: URI | string | undefined): void { + public $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void { // Force shellIntegration creation if it hasn't been created yet, this could when events // don't come through on startup if (!this._activeShellIntegrations.has(instanceId)) { this.$shellIntegrationChange(instanceId); } - this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, cwd); + const commandLine: vscode.TerminalShellExecutionCommandLine = { + value: commandLineValue, + confidence: commandLineConfidence, + isTrusted + }; + this._activeShellIntegrations.get(instanceId)?.startShellExecution(commandLine, URI.revive(cwd)); } - public $shellExecutionEnd(instanceId: number, commandLine: string | undefined, exitCode: number | undefined): void { + public $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void { + const commandLine: vscode.TerminalShellExecutionCommandLine = { + value: commandLineValue, + confidence: commandLineConfidence, + isTrusted + }; this._activeShellIntegrations.get(instanceId)?.endShellExecution(commandLine, exitCode); } @@ -118,8 +131,8 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH this._activeShellIntegrations.get(instanceId)?.emitData(data); } - public $cwdChange(instanceId: number, cwd: string): void { - this._activeShellIntegrations.get(instanceId)?.setCwd(cwd); + public $cwdChange(instanceId: number, cwd: UriComponents | undefined): void { + this._activeShellIntegrations.get(instanceId)?.setCwd(URI.revive(cwd)); } public $closeTerminal(instanceId: number): void { @@ -134,7 +147,7 @@ class InternalTerminalShellIntegration extends Disposable { get currentExecution(): InternalTerminalShellExecution | undefined { return this._currentExecution; } private _ignoreNextExecution: boolean = false; - private _cwd: URI | string | undefined; + private _cwd: URI | undefined; readonly store: DisposableStore = this._register(new DisposableStore()); @@ -144,39 +157,58 @@ class InternalTerminalShellIntegration extends Disposable { readonly onDidRequestChangeShellIntegration = this._onDidRequestChangeShellIntegration.event; protected readonly _onDidRequestShellExecution = this._register(new Emitter()); readonly onDidRequestShellExecution = this._onDidRequestShellExecution.event; - protected readonly _onDidRequestEndExecution = this._register(new Emitter()); + protected readonly _onDidRequestEndExecution = this._register(new Emitter()); readonly onDidRequestEndExecution = this._onDidRequestEndExecution.event; constructor( private readonly _terminal: vscode.Terminal, - private readonly _onDidStartTerminalShellExecution: Emitter + private readonly _onDidStartTerminalShellExecution: Emitter ) { super(); const that = this; this.value = { - get cwd(): URI | string | undefined { + get cwd(): URI | undefined { return that._cwd; }, - executeCommand(commandLine): vscode.TerminalShellExecution { - that._onDidRequestShellExecution.fire(commandLine); - const execution = that.startShellExecution(commandLine, that._cwd).value; + // executeCommand(commandLine: string): vscode.TerminalShellExecution; + // executeCommand(executable: string, args: string[]): vscode.TerminalShellExecution; + executeCommand(commandLineOrExecutable: string, args?: string[]): vscode.TerminalShellExecution { + let commandLineValue: string = commandLineOrExecutable; + if (args) { + commandLineValue += ` "${args.map(e => `${e.replaceAll('"', '\\"')}`).join('" "')}"`; + } + + that._onDidRequestShellExecution.fire(commandLineValue); + // Fire the event in a microtask to allow the extension to use the execution before + // the start event fires + const commandLine: vscode.TerminalShellExecutionCommandLine = { + value: commandLineValue, + confidence: TerminalShellExecutionCommandLineConfidence.High, + isTrusted: true + }; + const execution = that.startShellExecution(commandLine, that._cwd, true).value; that._ignoreNextExecution = true; return execution; } }; } - startShellExecution(commandLine: string, cwd: URI | string | undefined): InternalTerminalShellExecution { + startShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine, cwd: URI | undefined, fireEventInMicrotask?: boolean): InternalTerminalShellExecution { if (this._ignoreNextExecution && this._currentExecution) { this._ignoreNextExecution = false; } else { if (this._currentExecution) { - this._currentExecution.endExecution(undefined, undefined); - this._onDidRequestEndExecution.fire(this._currentExecution); + this._currentExecution.endExecution(undefined); + this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value, exitCode: undefined }); + } + // Fallback to the shell integration's cwd as the cwd may not have been restored after a reload + const currentExecution = this._currentExecution = new InternalTerminalShellExecution(commandLine, cwd ?? this._cwd); + if (fireEventInMicrotask) { + queueMicrotask(() => this._onDidStartTerminalShellExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: currentExecution.value })); + } else { + this._onDidStartTerminalShellExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value }); } - this._currentExecution = new InternalTerminalShellExecution(this._terminal, commandLine, cwd); - this._onDidStartTerminalShellExecution.fire(this._currentExecution.value); } return this._currentExecution; } @@ -185,20 +217,18 @@ class InternalTerminalShellIntegration extends Disposable { this.currentExecution?.emitData(data); } - endShellExecution(commandLine: string | undefined, exitCode: number | undefined): void { + endShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined, exitCode: number | undefined): void { if (this._currentExecution) { - this._currentExecution.endExecution(commandLine, exitCode); - this._onDidRequestEndExecution.fire(this._currentExecution); + this._currentExecution.endExecution(commandLine); + this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value, exitCode }); this._currentExecution = undefined; } } - setCwd(cwd: URI | string): void { + setCwd(cwd: URI | undefined): void { let wasChanged = false; if (URI.isUri(this._cwd)) { - if (this._cwd.toString() !== cwd.toString()) { - wasChanged = true; - } + wasChanged = !URI.isUri(cwd) || this._cwd.toString() !== cwd.toString(); } else if (this._cwd !== cwd) { wasChanged = true; } @@ -212,35 +242,23 @@ class InternalTerminalShellIntegration extends Disposable { class InternalTerminalShellExecution { private _dataStream: ShellExecutionDataStream | undefined; - private readonly _exitCode: Promise; - private _exitCodeResolve: ((exitCode: number | undefined) => void) | undefined; + private _ended: boolean = false; readonly value: vscode.TerminalShellExecution; constructor( - readonly terminal: vscode.Terminal, - private _commandLine: string | undefined, - readonly cwd: URI | string | undefined, + private _commandLine: vscode.TerminalShellExecutionCommandLine, + readonly cwd: URI | undefined, ) { - this._exitCode = new Promise(resolve => { - this._exitCodeResolve = resolve; - }); - const that = this; this.value = { - get terminal(): vscode.Terminal { - return terminal; - }, - get commandLine(): string | undefined { + get commandLine(): vscode.TerminalShellExecutionCommandLine { return that._commandLine; }, - get cwd(): URI | string | undefined { - return cwd; + get cwd(): URI | undefined { + return that.cwd; }, - get exitCode(): Promise { - return that._exitCode; - }, - createDataStream(): AsyncIterable { + read(): AsyncIterable { return that._createDataStream(); } }; @@ -248,7 +266,7 @@ class InternalTerminalShellExecution { private _createDataStream(): AsyncIterable { if (!this._dataStream) { - if (this._exitCodeResolve === undefined) { + if (this._ended) { return AsyncIterableObject.EMPTY; } this._dataStream = new ShellExecutionDataStream(); @@ -260,14 +278,13 @@ class InternalTerminalShellExecution { this._dataStream?.emitData(data); } - endExecution(commandLine: string | undefined, exitCode: number | undefined): void { + endExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined): void { if (commandLine) { this._commandLine = commandLine; } this._dataStream?.endExecution(); this._dataStream = undefined; - this._exitCodeResolve?.(exitCode); - this._exitCodeResolve = undefined; + this._ended = true; } } @@ -276,7 +293,10 @@ class ShellExecutionDataStream extends Disposable { private _emitters: AsyncIterableEmitter[] = []; createIterable(): AsyncIterable { - const barrier = this._barrier = new Barrier(); + if (!this._barrier) { + this._barrier = new Barrier(); + } + const barrier = this._barrier; const iterable = new AsyncIterableObject(async emitter => { this._emitters.push(emitter); await barrier.wait(); diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 3cd53e11d89..d269b34e120 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -14,48 +14,66 @@ import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { isDefined } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostTestingShape, ILocationDto, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; +import { FileCoverage, TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; -import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; +import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestControllerCapability, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; interface ControllerInfo { controller: vscode.TestController; profiles: Map; collection: ExtHostTestItemCollection; - extension: Readonly; + extension: IExtensionDescription; + relatedCodeProvider?: vscode.TestRelatedCodeProvider; activeProfiles: Set; } type DefaultProfileChangeEvent = Map>; +let followupCounter = 0; + +const testResultInternalIDs = new WeakMap(); + +export const IExtHostTesting = createDecorator('IExtHostTesting'); +export interface IExtHostTesting extends ExtHostTesting { + readonly _serviceBrand: undefined; +} + export class ExtHostTesting extends Disposable implements ExtHostTestingShape { + declare readonly _serviceBrand: undefined; + private readonly resultsChangedEmitter = this._register(new Emitter()); protected readonly controllers = new Map(); private readonly proxy: MainThreadTestingShape; private readonly runTracker: TestRunCoordinator; private readonly observer: TestObservers; private readonly defaultProfilesChangedEmitter = this._register(new Emitter()); + private readonly followupProviders = new Set(); + private readonly testFollowups = new Map(); public onResultsChanged = this.resultsChangedEmitter.event; public results: ReadonlyArray = []; constructor( @IExtHostRpcService rpc: IExtHostRpcService, - @ILogService logService: ILogService, - commands: ExtHostCommands, - private readonly editors: ExtHostDocumentsAndEditors, + @ILogService private readonly logService: ILogService, + @IExtHostCommands private readonly commands: IExtHostCommands, + @IExtHostDocumentsAndEditors private readonly editors: IExtHostDocumentsAndEditors, ) { super(); this.proxy = rpc.getProxy(MainContext.MainThreadTesting); @@ -104,6 +122,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }); } + //#region public API + /** * Implements vscode.test.registerTestProvider */ @@ -120,6 +140,23 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { const activeProfiles = new Set(); const proxy = this.proxy; + const getCapability = () => { + let cap = 0; + if (refreshHandler) { + cap |= TestControllerCapability.Refresh; + } + const rcp = info.relatedCodeProvider; + if (rcp) { + if (rcp?.provideRelatedTests) { + cap |= TestControllerCapability.TestRelatedToCode; + } + if (rcp?.provideRelatedCode) { + cap |= TestControllerCapability.CodeRelatedToTest; + } + } + return cap as TestControllerCapability; + }; + const controller: vscode.TestController = { items: collection.root.children, get label() { @@ -135,11 +172,19 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }, set refreshHandler(value: ((token: CancellationToken) => Thenable | void) | undefined) { refreshHandler = value; - proxy.$updateController(controllerId, { canRefresh: !!value }); + proxy.$updateController(controllerId, { capabilities: getCapability() }); }, get id() { return controllerId; }, + get relatedCodeProvider() { + return info.relatedCodeProvider; + }, + set relatedCodeProvider(value: vscode.TestRelatedCodeProvider | undefined) { + checkProposedApiEnabled(extension, 'testRelatedCode'); + info.relatedCodeProvider = value; + proxy.$updateController(controllerId, { capabilities: getCapability() }); + }, createRunProfile: (label, group, runHandler, isDefault, tag?: vscode.TestTag | undefined, supportsContinuousRun?: boolean) => { // Derive the profile ID from a hash so that the same profile will tend // to have the same hashes, allowing re-run requests to work across reloads. @@ -154,7 +199,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return new TestItemImpl(controllerId, id, label, uri); }, createTestRun: (request, name, persist = true) => { - return this.runTracker.createTestRun(controllerId, collection, request, name, persist); + return this.runTracker.createTestRun(extension, controllerId, collection, request, name, persist); }, invalidateTestResults: items => { if (items === undefined) { @@ -175,10 +220,10 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }, }; - proxy.$registerTestController(controllerId, label, !!refreshHandler); + const info: ControllerInfo = { controller, collection, profiles, extension, activeProfiles }; + proxy.$registerTestController(controllerId, label, getCapability()); disposable.add(toDisposable(() => proxy.$unregisterTestController(controllerId))); - const info: ControllerInfo = { controller, collection, profiles, extension, activeProfiles }; this.controllers.set(controllerId, info); disposable.add(toDisposable(() => this.controllers.delete(controllerId))); @@ -210,10 +255,10 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { } await this.proxy.$runTests({ - isUiTriggered: false, + preserveFocus: req.preserveFocus ?? true, + group: profileGroupToBitset[profile.kind], targets: [{ testIds: req.include?.map(t => TestId.fromExtHostTestItem(t, controller.collection.root.id).toString()) ?? [controller.collection.root.id], - profileGroup: profileGroupToBitset[profile.kind], profileId: profile.profileId, controllerId: profile.controllerId, }], @@ -221,6 +266,67 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }, token); } + /** + * Implements vscode.test.registerTestFollowupProvider + */ + public registerTestFollowupProvider(provider: vscode.TestFollowupProvider): vscode.Disposable { + this.followupProviders.add(provider); + return { dispose: () => { this.followupProviders.delete(provider); } }; + } + + //#endregion + + //#region RPC methods + /** + * @inheritdoc + */ + async $getTestsRelatedToCode(uri: UriComponents, _position: IPosition, token: CancellationToken): Promise { + const doc = this.editors.getDocument(URI.revive(uri)); + if (!doc) { + return []; + } + + const position = Convert.Position.to(_position); + const related: string[] = []; + await Promise.all([...this.controllers.values()].map(async (c) => { + let tests: vscode.TestItem[] | undefined | null; + try { + tests = await c.relatedCodeProvider?.provideRelatedTests?.(doc.document, position, token); + } catch (e) { + if (!token.isCancellationRequested) { + this.logService.warn(`Error thrown while providing related tests for ${c.controller.label}`, e); + } + } + + if (tests) { + for (const test of tests) { + related.push(TestId.fromExtHostTestItem(test, c.controller.id).toString()); + } + c.collection.flushDiff(); + } + })); + + return related; + } + + /** + * @inheritdoc + */ + async $getCodeRelatedToTest(testId: string, token: CancellationToken): Promise { + const controller = this.controllers.get(TestId.root(testId)); + if (!controller) { + return []; + } + + const test = controller.collection.tree.get(testId); + if (!test) { + return []; + } + + const locations = await controller.relatedCodeProvider?.provideRelatedCode?.(test.actual, token); + return locations?.map(Convert.location.from) ?? []; + } + /** * @inheritdoc */ @@ -235,8 +341,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { /** * @inheritdoc */ - async $getCoverageDetails(coverageId: string, token: CancellationToken): Promise { - const details = await this.runTracker.getCoverageDetails(coverageId, token); + async $getCoverageDetails(coverageId: string, testId: string | undefined, token: CancellationToken): Promise { + const details = await this.runTracker.getCoverageDetails(coverageId, testId, token); return details?.map(Convert.TestCoverage.fromDetails); } @@ -291,7 +397,11 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { public $publishTestResults(results: ISerializedTestResults[]): void { this.results = Object.freeze( results - .map(Convert.TestResults.to) + .map(r => { + const o = Convert.TestResults.to(r); + testResultInternalIDs.set(o, r.id); + return o; + }) .concat(this.results) .sort((a, b) => b.completedAt - a.completedAt) .slice(0, 32), @@ -347,13 +457,83 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return res; } + /** @inheritdoc */ + public async $provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise { + const results = this.results.find(r => testResultInternalIDs.get(r) === req.resultId); + const test = results && findTestInResultSnapshot(TestId.fromString(req.extId), results?.results); + if (!test) { + return []; + } + + let followups: vscode.Command[] = []; + await Promise.all([...this.followupProviders].map(async provider => { + try { + const r = await provider.provideFollowup(results, test, req.taskIndex, req.messageIndex, token); + if (r) { + followups = followups.concat(r); + } + } catch (e) { + this.logService.error(`Error thrown while providing followup for test message`, e); + } + })); + + if (token.isCancellationRequested) { + return []; + } + + return followups.map(command => { + const id = followupCounter++; + this.testFollowups.set(id, command); + return { title: command.title, id }; + }); + } + + $disposeTestFollowups(id: number[]): void { + for (const i of id) { + this.testFollowups.delete(i); + } + } + + $executeTestFollowup(id: number): Promise { + const command = this.testFollowups.get(id); + if (!command) { + return Promise.resolve(); + } + + return this.commands.executeCommand(command.command, ...(command.arguments || [])); + } + + /** + * Cancels an ongoing test run. + */ + public $cancelExtensionTestRun(runId: string | undefined, taskId: string | undefined) { + if (runId === undefined) { + this.runTracker.cancelAllRuns(); + } else { + this.runTracker.cancelRunById(runId, taskId); + } + } + + //#endregion + + public getMetadataForRun(run: vscode.TestRun) { + for (const tracker of this.runTracker.trackers) { + const taskId = tracker.getTaskIdForRun(run); + if (taskId) { + return { taskId, runId: tracker.id }; + } + } + + return undefined; + } + private async runControllerTestRequest(req: ICallProfileRunHandler | ICallProfileRunHandler, isContinuous: boolean, token: CancellationToken): Promise { const lookup = this.controllers.get(req.controllerId); if (!lookup) { return {}; } - const { collection, profiles } = lookup; + const { collection, profiles, extension } = lookup; const profile = profiles.get(req.profileId); if (!profile) { return {}; @@ -382,6 +562,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { ); const tracker = isStartControllerTests(req) && this.runTracker.prepareForMainThreadTestRun( + extension, publicReq, TestRunDto.fromInternal(req, lookup.collection), profile, @@ -401,17 +582,6 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { } } } - - /** - * Cancels an ongoing test run. - */ - public $cancelExtensionTestRun(runId: string | undefined) { - if (runId === undefined) { - this.runTracker.cancelAllRuns(); - } else { - this.runTracker.cancelRunById(runId); - } - } } // Deadline after being requested by a user that a test run is forcibly cancelled. @@ -429,15 +599,12 @@ const enum TestRunTrackerState { class TestRunTracker extends Disposable { private state = TestRunTrackerState.Running; private running = 0; - private readonly tasks = new Map(); + private readonly tasks = new Map(); private readonly sharedTestIds = new Set(); private readonly cts: CancellationTokenSource; private readonly endEmitter = this._register(new Emitter()); private readonly onDidDispose: Event; - private readonly publishedCoverage = new Map Thenable; - }>(); + private readonly publishedCoverage = new Map(); /** * Fires when a test ends, and no more tests are left running. @@ -463,6 +630,7 @@ class TestRunTracker extends Disposable { private readonly proxy: MainThreadTestingShape, private readonly logService: ILogService, private readonly profile: vscode.TestRunProfile | undefined, + private readonly extension: IExtensionDescription, parentToken?: CancellationToken, ) { super(); @@ -479,9 +647,22 @@ class TestRunTracker extends Disposable { })); } + /** Gets the task ID from a test run object. */ + public getTaskIdForRun(run: vscode.TestRun) { + for (const [taskId, { run: r }] of this.tasks) { + if (r === run) { + return taskId; + } + } + + return undefined; + } + /** Requests cancellation of the run. On the second call, forces cancellation. */ - public cancel() { - if (this.state === TestRunTrackerState.Running) { + public cancel(taskId?: string) { + if (taskId) { + this.tasks.get(taskId)?.cts.cancel(); + } else if (this.state === TestRunTrackerState.Running) { this.cts.cancel(); this.state = TestRunTrackerState.Cancelling; } else if (this.state === TestRunTrackerState.Cancelling) { @@ -490,23 +671,33 @@ class TestRunTracker extends Disposable { } /** Gets details for a previously-emitted coverage object. */ - public getCoverageDetails(id: string, token: CancellationToken) { - const [, taskId, covId] = TestId.fromString(id).path; /** runId, taskId, URI */ - const obj = this.publishedCoverage.get(covId); - if (!obj) { + public async getCoverageDetails(id: string, testId: string | undefined, token: CancellationToken): Promise { + const [, taskId] = TestId.fromString(id).path; /** runId, taskId, URI */ + const coverage = this.publishedCoverage.get(id); + if (!coverage) { return []; } - if (obj.backCompatResolve) { - return obj.backCompatResolve(token); - } - + const { report, extIds } = coverage; const task = this.tasks.get(taskId); if (!task) { throw new Error('unreachable: run task was not found'); } - return this.profile?.loadDetailedCoverage?.(task.run, obj.coverage, token) ?? []; + let testItem: vscode.TestItem | undefined; + if (testId && report instanceof FileCoverage) { + const index = extIds.indexOf(testId); + if (index === -1) { + return []; // ?? + } + testItem = report.fromTests[index]; + } + + const details = testItem + ? this.profile?.loadDetailedCoverageForTest?.(task.run, report, testItem, token) + : this.profile?.loadDetailedCoverage?.(task.run, report, token); + + return (await details) ?? []; } /** Creates the public test run interface to give to extensions. */ @@ -522,10 +713,6 @@ class TestRunTracker extends Disposable { return; } - if (!this.dto.isIncluded(test)) { - return; - } - this.ensureTestIsKnown(test); fn(test, ...args); }; @@ -545,42 +732,40 @@ class TestRunTracker extends Disposable { this.proxy.$appendTestMessagesInRun(runId, taskId, TestId.fromExtHostTestItem(test, ctrlId).toString(), converted); }; - const addCoverage = (coverage: vscode.FileCoverage, backCompatResolve?: (token: vscode.CancellationToken) => Thenable) => { - const uriStr = coverage.uri.toString(); - const id = new TestId([runId, taskId, uriStr]).toString(); - this.publishedCoverage.set(uriStr, { coverage, backCompatResolve }); - this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(id, coverage)); - }; - - interface ICoverageProvider { - provideFileCoverage(token: CancellationToken): vscode.ProviderResult; - resolveFileCoverage?(coverage: vscode.FileCoverage, token: CancellationToken): vscode.ProviderResult; - } - let ended = false; - let coverageProvider: ICoverageProvider | undefined; - const run: vscode.TestRun & { coverageProvider?: ICoverageProvider } = { + // tasks are alive for as long as the tracker is alive, so simple this._register is fine: + const cts = this._register(new CancellationTokenSource(this.cts.token)); + + // one-off map used to associate test items with incrementing IDs in `addCoverage`. + // There's no need to include their entire ID, we just want to make sure they're + // stable and unique. Normal map is okay since TestRun lifetimes are limited. + const run: vscode.TestRun = { isPersisted: this.dto.isPersisted, - token: this.cts.token, + token: cts.token, name, onDidDispose: this.onDidDispose, - // todo@connor4312: back compat - get coverageProvider() { - return coverageProvider; - }, - // todo@connor4312: back compat - set coverageProvider(provider: ICoverageProvider | undefined) { - coverageProvider = provider; - if (provider) { - Promise.resolve(provider.provideFileCoverage(CancellationToken.None)).then(coverage => { - coverage?.forEach(c => addCoverage(c, provider.resolveFileCoverage && (async token => { - const r = await provider.resolveFileCoverage!(c, token); - return (r || c as any).detailedCoverage; - }))); - }); + addCoverage: (coverage) => { + if (ended) { + return; } + + const fromTests = coverage instanceof FileCoverage ? coverage.fromTests : []; + if (fromTests.length) { + checkProposedApiEnabled(this.extension, 'attributableCoverage'); + for (const test of fromTests) { + this.ensureTestIsKnown(test); + } + } + + const uriStr = coverage.uri.toString(); + const id = new TestId([runId, taskId, uriStr]).toString(); + // it's a lil funky, but it's possible for a test item's ID to change after + // it's been reported if it's rehomed under a different parent. Record its + // ID at the time when the coverage report is generated so we can reference + // it later if needeed. + this.publishedCoverage.set(id, { report: coverage, extIds: fromTests.map(t => TestId.fromExtHostTestItem(t, ctrlId).toString()) }); + this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(ctrlId, id, coverage)); }, - addCoverage, //#region state mutation enqueued: guardTestMutation(test => { this.proxy.$updateTestStateInRun(runId, taskId, TestId.fromExtHostTestItem(test, ctrlId).toString(), TestResultState.Queued); @@ -609,11 +794,7 @@ class TestRunTracker extends Disposable { } if (test) { - if (this.dto.isIncluded(test)) { - this.ensureTestIsKnown(test); - } else { - test = undefined; - } + this.ensureTestIsKnown(test); } this.proxy.$appendOutputToRun( @@ -638,8 +819,13 @@ class TestRunTracker extends Disposable { }; this.running++; - this.tasks.set(taskId, { run }); - this.proxy.$startedTestRunTask(runId, { id: taskId, name, running: true }); + this.tasks.set(taskId, { run, cts }); + this.proxy.$startedTestRunTask(runId, { + id: taskId, + ctrlId: this.dto.controllerId, + name: name || this.extension.displayName || this.extension.identifier.value, + running: true, + }); return run; } @@ -713,9 +899,9 @@ export class TestRunCoordinator { /** * Gets a coverage report for a given run and task ID. */ - public getCoverageDetails(id: string, token: vscode.CancellationToken) { + public getCoverageDetails(id: string, testId: string | undefined, token: vscode.CancellationToken) { const runId = TestId.root(id); - return this.trackedById.get(runId)?.getCoverageDetails(id, token) || []; + return this.trackedById.get(runId)?.getCoverageDetails(id, testId, token) || []; } /** @@ -737,15 +923,15 @@ export class TestRunCoordinator { * `$startedExtensionTestRun` is not invoked. The run must eventually * be cancelled manually. */ - public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { - return this.getTracker(req, dto, profile, token); + public prepareForMainThreadTestRun(extension: IExtensionDescription, req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { + return this.getTracker(req, dto, profile, extension, token); } /** * Cancels an existing test run via its cancellation token. */ - public cancelRunById(runId: string) { - this.trackedById.get(runId)?.cancel(); + public cancelRunById(runId: string, taskId?: string) { + this.trackedById.get(runId)?.cancel(taskId); } /** @@ -760,7 +946,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(extension: IExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -777,10 +963,11 @@ export class TestRunCoordinator { exclude: request.exclude?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [], id: dto.id, include: request.include?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [collection.root.id], + preserveFocus: request.preserveFocus ?? true, persist }); - const tracker = this.getTracker(request, dto, request.profile); + const tracker = this.getTracker(request, dto, request.profile, extension); Event.once(tracker.onEnd)(() => { this.proxy.$finishedExtensionTestRun(dto.id); }); @@ -788,8 +975,8 @@ export class TestRunCoordinator { return tracker.createRun(name); } - private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, token?: CancellationToken) { - const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, token); + private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, extension: IExtensionDescription, token?: CancellationToken) { + const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, extension, token); this.tracked.set(req, tracker); this.trackedById.set(tracker.id, tracker); return tracker; @@ -809,15 +996,10 @@ const tryGetProfileFromTestRunReq = (request: vscode.TestRunRequest) => { }; export class TestRunDto { - private readonly includePrefix: string[]; - private readonly excludePrefix: string[]; - public static fromPublic(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, persist: boolean) { return new TestRunDto( controllerId, generateUuid(), - request.include?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [controllerId], - request.exclude?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [], persist, collection, ); @@ -827,8 +1009,6 @@ export class TestRunDto { return new TestRunDto( request.controllerId, request.runId, - request.testIds, - request.excludeExtIds, true, collection, ); @@ -837,30 +1017,9 @@ export class TestRunDto { constructor( public readonly controllerId: string, public readonly id: string, - include: string[], - exclude: string[], public readonly isPersisted: boolean, public readonly colllection: ExtHostTestItemCollection, ) { - this.includePrefix = include.map(id => id + TestIdPathParts.Delimiter); - this.excludePrefix = exclude.map(id => id + TestIdPathParts.Delimiter); - } - - public isIncluded(test: vscode.TestItem) { - const id = TestId.fromExtHostTestItem(test, this.controllerId).toString() + TestIdPathParts.Delimiter; - for (const prefix of this.excludePrefix) { - if (id === prefix || id.startsWith(prefix)) { - return false; - } - } - - for (const prefix of this.includePrefix) { - if (id === prefix || id.startsWith(prefix)) { - return true; - } - } - - return false; } } @@ -1200,3 +1359,20 @@ const profileGroupToBitset: { [K in TestRunProfileKind]: TestRunProfileBitset } [TestRunProfileKind.Debug]: TestRunProfileBitset.Debug, [TestRunProfileKind.Run]: TestRunProfileBitset.Run, }; + +function findTestInResultSnapshot(extId: TestId, snapshot: readonly Readonly[]) { + for (let i = 0; i < extId.path.length; i++) { + const item = snapshot.find(s => s.id === extId.path[i]); + if (!item) { + return undefined; + } + + if (i === extId.path.length - 1) { + return item; + } + + snapshot = item.children; + } + + return undefined; +} diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 44ed8f3804f..73334e5ac89 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -566,6 +566,9 @@ export class ExtHostTextEditor { }, hide() { _proxy.$tryHideEditor(id); + }, + [Symbol.for('debug.description')]() { + return `TextEditor(${this.document.uri.toString()})`; } }); } diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 7ab8d65df53..277422f9acb 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -5,6 +5,7 @@ import * as arrays from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; @@ -13,7 +14,7 @@ import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; -export class ExtHostEditors implements ExtHostEditorsShape { +export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { private readonly _onDidChangeTextEditorSelection = new Emitter(); private readonly _onDidChangeTextEditorOptions = new Emitter(); @@ -35,11 +36,11 @@ export class ExtHostEditors implements ExtHostEditorsShape { mainContext: IMainContext, private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, ) { + super(); this._proxy = mainContext.getProxy(MainContext.MainThreadTextEditors); - - this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e)); - this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); + this._register(this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e))); + this._register(this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e))); } getActiveTextEditor(): vscode.TextEditor | undefined { diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 2ea75f8b775..ba5e9aa7c50 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -355,7 +355,12 @@ class ExtHostTreeView extends Disposable { this.dataProvider = options.treeDataProvider; this.dndController = options.dragAndDropController; if (this.dataProvider.onDidChangeTreeData) { - this._register(this.dataProvider.onDidChangeTreeData(elementOrElements => this._onDidChangeData.fire({ message: false, element: elementOrElements }))); + this._register(this.dataProvider.onDidChangeTreeData(elementOrElements => { + if (Array.isArray(elementOrElements) && elementOrElements.length === 0) { + return; + } + this._onDidChangeData.fire({ message: false, element: elementOrElements }); + })); } let refreshingPromise: Promise | null; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index caf7fcd3607..e062e5e0267 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -16,6 +16,7 @@ import { Mimes } from 'vs/base/common/mime'; import { cloneAndChange } from 'vs/base/common/objects'; import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { basename } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; import { isDefined, isEmptyObject, isNumber, isString, isUndefinedOrNull } from 'vs/base/common/types'; import { URI, UriComponents, isUriComponents } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; @@ -39,11 +40,9 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTreeData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; -import { IInlineChatCommandFollowup, IInlineChatFollowup, IInlineChatReplyFollowup, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import * as search from 'vs/workbench/contrib/search/common/search'; @@ -51,10 +50,10 @@ import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestTag, TestMessageType, TestResultItem, denamespaceTestTag, namespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import * as types from './extHostTypes'; +import { IToolData, IToolResult } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; export namespace Command { @@ -578,7 +577,7 @@ export namespace DecorationRenderOptions { export namespace TextEdit { export function from(edit: vscode.TextEdit): languages.TextEdit { - return { + return { text: edit.newText, eol: edit.newEol && EndOfLine.from(edit.newEol), range: Range.from(edit.range) @@ -788,7 +787,7 @@ export namespace SymbolTag { export namespace WorkspaceSymbol { export function from(info: vscode.SymbolInformation): search.IWorkspaceSymbol { - return { + return { name: info.name, kind: SymbolKind.from(info.kind), tags: info.tags && info.tags.map(SymbolTag.from), @@ -946,21 +945,28 @@ export namespace DefinitionLink { } export namespace Hover { - export function from(hover: vscode.Hover): languages.Hover { - return { + export function from(hover: vscode.VerboseHover): languages.Hover { + const convertedHover: languages.Hover = { range: Range.from(hover.range), - contents: MarkdownString.fromMany(hover.contents) + contents: MarkdownString.fromMany(hover.contents), + canIncreaseVerbosity: hover.canIncreaseVerbosity, + canDecreaseVerbosity: hover.canDecreaseVerbosity, }; + return convertedHover; } - export function to(info: languages.Hover): types.Hover { - return new types.Hover(info.contents.map(MarkdownString.to), Range.to(info.range)); + export function to(info: languages.Hover): types.VerboseHover { + const contents = info.contents.map(MarkdownString.to); + const range = Range.to(info.range); + const canIncreaseVerbosity = info.canIncreaseVerbosity; + const canDecreaseVerbosity = info.canDecreaseVerbosity; + return new types.VerboseHover(contents, range, canIncreaseVerbosity, canDecreaseVerbosity); } } export namespace EvaluatableExpression { export function from(expression: vscode.EvaluatableExpression): languages.EvaluatableExpression { - return { + return { range: Range.from(expression.range), expression: expression.expression }; @@ -974,24 +980,24 @@ export namespace EvaluatableExpression { export namespace InlineValue { export function from(inlineValue: vscode.InlineValue): languages.InlineValue { if (inlineValue instanceof types.InlineValueText) { - return { + return { type: 'text', range: Range.from(inlineValue.range), text: inlineValue.text - }; + } satisfies languages.InlineValueText; } else if (inlineValue instanceof types.InlineValueVariableLookup) { - return { + return { type: 'variable', range: Range.from(inlineValue.range), variableName: inlineValue.variableName, caseSensitiveLookup: inlineValue.caseSensitiveLookup - }; + } satisfies languages.InlineValueVariableLookup; } else if (inlineValue instanceof types.InlineValueEvaluatableExpression) { - return { + return { type: 'expression', range: Range.from(inlineValue.range), expression: inlineValue.expression - }; + } satisfies languages.InlineValueExpression; } else { throw new Error(`Unknown 'InlineValue' type`); } @@ -1000,28 +1006,28 @@ export namespace InlineValue { export function to(inlineValue: languages.InlineValue): vscode.InlineValue { switch (inlineValue.type) { case 'text': - return { + return { range: Range.to(inlineValue.range), text: inlineValue.text - }; + } satisfies vscode.InlineValueText; case 'variable': - return { + return { range: Range.to(inlineValue.range), variableName: inlineValue.variableName, caseSensitiveLookup: inlineValue.caseSensitiveLookup - }; + } satisfies vscode.InlineValueVariableLookup; case 'expression': - return { + return { range: Range.to(inlineValue.range), expression: inlineValue.expression - }; + } satisfies vscode.InlineValueEvaluatableExpression; } } } export namespace InlineValueContext { export function from(inlineValueContext: vscode.InlineValueContext): extHostProtocol.IInlineValueContextDto { - return { + return { frameId: inlineValueContext.frameId, stoppedLocation: Range.from(inlineValueContext.stoppedLocation) }; @@ -1325,7 +1331,9 @@ export namespace DocumentLink { // ignore } } - return new types.DocumentLink(Range.to(link.range), target); + const result = new types.DocumentLink(Range.to(link.range), target); + result.tooltip = link.tooltip; + return result; } } @@ -1880,6 +1888,11 @@ export namespace TestMessage { actual: message.actualOutput, contextValue: message.contextValue, location: message.location && ({ range: Range.from(message.location.range), uri: message.location.uri }), + stackTrace: (message as vscode.TestMessage2).stackTrace?.map(s => ({ + label: s.label, + position: s.position && Position.from(s.position), + uri: s.uri && URI.revive(s.uri).toJSON(), + })), }; } @@ -2047,7 +2060,7 @@ export namespace TestCoverage { } } - export function fromFile(id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { + export function fromFile(controllerId: string, id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { types.validateTestCoverageCount(coverage.statementCoverage); types.validateTestCoverageCount(coverage.branchCoverage); types.validateTestCoverageCount(coverage.declarationCoverage); @@ -2058,6 +2071,8 @@ export namespace TestCoverage { statement: fromCoverageCount(coverage.statementCoverage), branch: coverage.branchCoverage && fromCoverageCount(coverage.branchCoverage), declaration: coverage.declarationCoverage && fromCoverageCount(coverage.declarationCoverage), + testIds: coverage instanceof types.FileCoverage && coverage.fromTests.length ? + coverage.fromTests.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) : undefined, }; } } @@ -2234,135 +2249,124 @@ export namespace ChatFollowup { } } -export namespace ChatInlineFollowup { - export function from(followup: vscode.InteractiveEditorFollowup): IInlineChatFollowup { - if ('commandId' in followup) { - return { - kind: 'command', - title: followup.title ?? '', - commandId: followup.commandId ?? '', - when: followup.when ?? '', - args: followup.args - } satisfies IInlineChatCommandFollowup; - } else { - return { - kind: 'reply', - message: followup.message, - title: followup.title, - tooltip: followup.tooltip, - } satisfies IInlineChatReplyFollowup; +export namespace LanguageModelChatMessageRole { + export function to(role: chatProvider.ChatMessageRole): vscode.LanguageModelChatMessageRole { + switch (role) { + case chatProvider.ChatMessageRole.System: return types.LanguageModelChatMessageRole.System; + case chatProvider.ChatMessageRole.User: return types.LanguageModelChatMessageRole.User; + case chatProvider.ChatMessageRole.Assistant: return types.LanguageModelChatMessageRole.Assistant; } + } + export function from(role: vscode.LanguageModelChatMessageRole): chatProvider.ChatMessageRole { + switch (role) { + case types.LanguageModelChatMessageRole.System: return chatProvider.ChatMessageRole.System; + case types.LanguageModelChatMessageRole.User: return chatProvider.ChatMessageRole.User; + case types.LanguageModelChatMessageRole.Assistant: return chatProvider.ChatMessageRole.Assistant; + } + return chatProvider.ChatMessageRole.User; } } -export namespace LanguageModelMessage { +export namespace LanguageModelChatMessage { export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { - switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatSystemMessage(message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatUserMessage(message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatAssistantMessage(message.content); - } - } - - export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { - if (message instanceof types.LanguageModelChatSystemMessage) { - return { role: chatProvider.ChatMessageRole.System, content: message.content }; - } else if (message instanceof types.LanguageModelChatUserMessage) { - return { role: chatProvider.ChatMessageRole.User, content: message.content }; - } else if (message instanceof types.LanguageModelChatAssistantMessage) { - return { role: chatProvider.ChatMessageRole.Assistant, content: message.content }; + let content: string = ''; + let content2: vscode.LanguageModelChatMessageFunctionResultPart | undefined; + if (message.content.type === 'text') { + content = message.content.value; } else { - throw new Error('Invalid LanguageModelMessage'); + content2 = new types.LanguageModelFunctionResultPart(message.content.name, message.content.value, message.content.isError); } - } -} - -export namespace ChatVariable { - export function objectTo(variableObject: Record): Record { - const result: Record = {}; - for (const key of Object.keys(variableObject)) { - result[key] = variableObject[key].map(ChatVariable.to); + const role = LanguageModelChatMessageRole.to(message.role); + const result = new types.LanguageModelChatMessage(role, content, message.name); + if (content2 !== undefined) { + result.content2 = content2; } - return result; } - export function to(variable: IChatRequestVariableValue): vscode.ChatVariableValue { + export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { + + const role = LanguageModelChatMessageRole.from(message.role); + const name = message.name; + + let content: chatProvider.IChatMessagePart; + + if (message.content2 instanceof types.LanguageModelFunctionResultPart) { + content = { + type: 'function_result', + name: message.content2.name, + value: message.content2.content, + isError: message.content2.isError + }; + } else { + content = { + type: 'text', + value: message.content + }; + } + return { - level: ChatVariableLevel.to(variable.level), - kind: variable.kind, - value: isUriComponents(variable.value) ? URI.revive(variable.value) : variable.value, - description: variable.description + role, + name, + content }; } - - export function from(variable: vscode.ChatVariableValue): IChatRequestVariableValue { - return { - level: ChatVariableLevel.from(variable.level), - kind: variable.kind, - value: variable.value, - description: variable.description - }; - } -} - -export namespace ChatVariableLevel { - - - export function to(level: 'short' | 'medium' | 'full'): vscode.ChatVariableLevel { - switch (level) { - case 'short': return types.ChatVariableLevel.Short; - case 'medium': return types.ChatVariableLevel.Medium; - case 'full': - default: - return types.ChatVariableLevel.Full; - } - } - export function from(level: vscode.ChatVariableLevel): 'short' | 'medium' | 'full' { - switch (level) { - case types.ChatVariableLevel.Short: return 'short'; - case types.ChatVariableLevel.Medium: return 'medium'; - case types.ChatVariableLevel.Full: - default: - return 'full'; - } - } -} - -export namespace InteractiveEditorResponseFeedbackKind { - - export function to(kind: InlineChatResponseFeedbackKind): vscode.InteractiveEditorResponseFeedbackKind { - switch (kind) { - case InlineChatResponseFeedbackKind.Helpful: - return types.InteractiveEditorResponseFeedbackKind.Helpful; - case InlineChatResponseFeedbackKind.Unhelpful: - return types.InteractiveEditorResponseFeedbackKind.Unhelpful; - case InlineChatResponseFeedbackKind.Undone: - return types.InteractiveEditorResponseFeedbackKind.Undone; - case InlineChatResponseFeedbackKind.Accepted: - return types.InteractiveEditorResponseFeedbackKind.Accepted; - case InlineChatResponseFeedbackKind.Bug: - return types.InteractiveEditorResponseFeedbackKind.Bug; - } - } } export namespace ChatResponseMarkdownPart { - export function to(part: vscode.ChatResponseMarkdownPart): Dto { + export function from(part: vscode.ChatResponseMarkdownPart): Dto { return { kind: 'markdownContent', content: MarkdownString.from(part.value) }; } - export function from(part: Dto): vscode.ChatResponseMarkdownPart { + export function to(part: Dto): vscode.ChatResponseMarkdownPart { return new types.ChatResponseMarkdownPart(MarkdownString.to(part.content)); } } +export namespace ChatResponseMarkdownWithVulnerabilitiesPart { + export function from(part: vscode.ChatResponseMarkdownWithVulnerabilitiesPart): Dto { + return { + kind: 'markdownVuln', + content: MarkdownString.from(part.value), + vulnerabilities: part.vulnerabilities, + }; + } + export function to(part: Dto): vscode.ChatResponseMarkdownWithVulnerabilitiesPart { + return new types.ChatResponseMarkdownWithVulnerabilitiesPart(MarkdownString.to(part.content), part.vulnerabilities); + } +} + +export namespace ChatResponseDetectedParticipantPart { + export function from(part: vscode.ChatResponseDetectedParticipantPart): Dto { + return { + kind: 'agentDetection', + agentId: part.participant, + command: part.command, + }; + } + export function to(part: Dto): vscode.ChatResponseDetectedParticipantPart { + return new types.ChatResponseDetectedParticipantPart(part.agentId, part.command); + } +} + +export namespace ChatResponseConfirmationPart { + export function from(part: vscode.ChatResponseConfirmationPart): Dto { + return { + kind: 'confirmation', + title: part.title, + message: part.message, + data: part.data, + buttons: part.buttons + }; + } +} + export namespace ChatResponseFilesPart { - export function to(part: vscode.ChatResponseFileTreePart): IChatTreeData { + export function from(part: vscode.ChatResponseFileTreePart): IChatTreeData { const { value, baseUri } = part; function convert(items: vscode.ChatResponseFileTree[], baseUri: URI): extHostProtocol.IChatResponseProgressFileTreeData[] { return items.map(item => { @@ -2383,7 +2387,7 @@ export namespace ChatResponseFilesPart { } }; } - export function from(part: Dto): vscode.ChatResponseFileTreePart { + export function to(part: Dto): vscode.ChatResponseFileTreePart { const treeData = revive(part.treeData); function convert(items: extHostProtocol.IChatResponseProgressFileTreeData[]): vscode.ChatResponseFileTree[] { return items.map(item => { @@ -2401,15 +2405,18 @@ export namespace ChatResponseFilesPart { } export namespace ChatResponseAnchorPart { - export function to(part: vscode.ChatResponseAnchorPart): Dto { + export function from(part: vscode.ChatResponseAnchorPart): Dto { + // Work around type-narrowing confusion between vscode.Uri and URI + const isUri = (thing: unknown): thing is vscode.Uri => URI.isUri(thing); + return { kind: 'inlineReference', name: part.title, - inlineReference: !URI.isUri(part.value) ? Location.from(part.value) : part.value + inlineReference: isUri(part.value) ? part.value : Location.from(part.value) }; } - export function from(part: Dto): vscode.ChatResponseAnchorPart { + export function to(part: Dto): vscode.ChatResponseAnchorPart { const value = revive(part); return new types.ChatResponseAnchorPart( URI.isUri(value.inlineReference) ? value.inlineReference : Location.to(value.inlineReference), @@ -2419,19 +2426,49 @@ export namespace ChatResponseAnchorPart { } export namespace ChatResponseProgressPart { - export function to(part: vscode.ChatResponseProgressPart): Dto { + export function from(part: vscode.ChatResponseProgressPart): Dto { return { kind: 'progressMessage', content: MarkdownString.from(part.value) }; } - export function from(part: Dto): vscode.ChatResponseProgressPart { + export function to(part: Dto): vscode.ChatResponseProgressPart { return new types.ChatResponseProgressPart(part.content.value); } } +export namespace ChatResponseWarningPart { + export function from(part: vscode.ChatResponseWarningPart): Dto { + return { + kind: 'warning', + content: MarkdownString.from(part.value) + }; + } + export function to(part: Dto): vscode.ChatResponseWarningPart { + return new types.ChatResponseWarningPart(part.content.value); + } +} + +export namespace ChatTask { + export function from(part: vscode.ChatResponseProgressPart2): IChatTaskDto { + return { + kind: 'progressTask', + content: MarkdownString.from(part.value), + }; + } +} + +export namespace ChatTaskResult { + export function from(part: string | void): Dto { + return { + kind: 'progressTaskResult', + content: typeof part === 'string' ? MarkdownString.from(part) : undefined + }; + } +} + export namespace ChatResponseCommandButtonPart { - export function to(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { + export function from(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore const command = commandsConverter.toInternal(part.value, commandDisposables) ?? { command: part.value.command, title: part.value.title }; return { @@ -2439,14 +2476,32 @@ export namespace ChatResponseCommandButtonPart { command }; } - export function from(part: Dto, commandsConverter: CommandsConverter): vscode.ChatResponseCommandButtonPart { + export function to(part: Dto, commandsConverter: CommandsConverter): vscode.ChatResponseCommandButtonPart { // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore return new types.ChatResponseCommandButtonPart(commandsConverter.fromInternal(part.command) ?? { command: part.command.id, title: part.command.title }); } } +export namespace ChatResponseTextEditPart { + export function from(part: vscode.ChatResponseTextEditPart): Dto { + return { + kind: 'textEdit', + uri: part.uri, + edits: part.edits.map(e => TextEdit.from(e)) + }; + } + export function to(part: Dto): vscode.ChatResponseTextEditPart { + return new types.ChatResponseTextEditPart(URI.revive(part.uri), part.edits.map(e => TextEdit.to(e))); + } + +} + export namespace ChatResponseReferencePart { - export function to(part: vscode.ChatResponseReferencePart): Dto { + export function from(part: types.ChatResponseReferencePart): Dto { + const iconPath = ThemeIcon.isThemeIcon(part.iconPath) ? part.iconPath + : URI.isUri(part.iconPath) ? { light: URI.revive(part.iconPath) } + : (part.iconPath && 'light' in part.iconPath && 'dark' in part.iconPath && URI.isUri(part.iconPath.light) && URI.isUri(part.iconPath.dark) ? { light: URI.revive(part.iconPath.light), dark: URI.revive(part.iconPath.dark) } + : undefined); if ('variableName' in part.value) { return { kind: 'reference', @@ -2455,7 +2510,9 @@ export namespace ChatResponseReferencePart { value: URI.isUri(part.value.value) || !part.value.value ? part.value.value : Location.from(part.value.value as vscode.Location) - } + }, + iconPath, + options: part.options }; } @@ -2463,10 +2520,12 @@ export namespace ChatResponseReferencePart { kind: 'reference', reference: URI.isUri(part.value) ? part.value : - Location.from(part.value) + Location.from(part.value), + iconPath, + options: part.options }; } - export function from(part: Dto): vscode.ChatResponseReferencePart { + export function to(part: Dto): vscode.ChatResponseReferencePart { const value = revive(part); const mapValue = (value: URI | languages.Location): vscode.Uri | vscode.Location => URI.isUri(value) ? @@ -2479,146 +2538,97 @@ export namespace ChatResponseReferencePart { value: value.reference.value && mapValue(value.reference.value) } : mapValue(value.reference) - ); + ) as vscode.ChatResponseReferencePart; // 'value' is extended with variableName + } +} + +export namespace ChatResponseCodeCitationPart { + export function from(part: vscode.ChatResponseCodeCitationPart): Dto { + return { + kind: 'codeCitation', + value: part.value, + license: part.license, + snippet: part.snippet + }; } } export namespace ChatResponsePart { - export function to(part: vscode.ChatResponsePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { - return ChatResponseMarkdownPart.to(part); + return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { - return ChatResponseAnchorPart.to(part); + return ChatResponseAnchorPart.from(part); } else if (part instanceof types.ChatResponseReferencePart) { - return ChatResponseReferencePart.to(part); + return ChatResponseReferencePart.from(part); } else if (part instanceof types.ChatResponseProgressPart) { - return ChatResponseProgressPart.to(part); + return ChatResponseProgressPart.from(part); } else if (part instanceof types.ChatResponseFileTreePart) { - return ChatResponseFilesPart.to(part); + return ChatResponseFilesPart.from(part); } else if (part instanceof types.ChatResponseCommandButtonPart) { - return ChatResponseCommandButtonPart.to(part, commandsConverter, commandDisposables); + return ChatResponseCommandButtonPart.from(part, commandsConverter, commandDisposables); + } else if (part instanceof types.ChatResponseTextEditPart) { + return ChatResponseTextEditPart.from(part); + } else if (part instanceof types.ChatResponseMarkdownWithVulnerabilitiesPart) { + return ChatResponseMarkdownWithVulnerabilitiesPart.from(part); + } else if (part instanceof types.ChatResponseDetectedParticipantPart) { + return ChatResponseDetectedParticipantPart.from(part); + } else if (part instanceof types.ChatResponseWarningPart) { + return ChatResponseWarningPart.from(part); + } else if (part instanceof types.ChatResponseConfirmationPart) { + return ChatResponseConfirmationPart.from(part); + } else if (part instanceof types.ChatResponseCodeCitationPart) { + return ChatResponseCodeCitationPart.from(part); } - return { - kind: 'content', - content: '' - }; + return { + kind: 'markdownContent', + content: MarkdownString.from('') + }; } - export function from(part: extHostProtocol.IChatProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponsePart | undefined { + export function to(part: extHostProtocol.IChatProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponsePart | undefined { switch (part.kind) { - case 'reference': return ChatResponseReferencePart.from(part); + case 'reference': return ChatResponseReferencePart.to(part); case 'markdownContent': case 'inlineReference': case 'progressMessage': case 'treeData': case 'command': - return fromContent(part, commandsConverter); + return toContent(part, commandsConverter); } return undefined; } - export function fromContent(part: extHostProtocol.IChatContentProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponseMarkdownPart | vscode.ChatResponseFileTreePart | vscode.ChatResponseAnchorPart | vscode.ChatResponseCommandButtonPart | undefined { + export function toContent(part: extHostProtocol.IChatContentProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponseMarkdownPart | vscode.ChatResponseFileTreePart | vscode.ChatResponseAnchorPart | vscode.ChatResponseCommandButtonPart | undefined { switch (part.kind) { - case 'markdownContent': return ChatResponseMarkdownPart.from(part); - case 'inlineReference': return ChatResponseAnchorPart.from(part); + case 'markdownContent': return ChatResponseMarkdownPart.to(part); + case 'inlineReference': return ChatResponseAnchorPart.to(part); case 'progressMessage': return undefined; - case 'treeData': return ChatResponseFilesPart.from(part); - case 'command': return ChatResponseCommandButtonPart.from(part, commandsConverter); + case 'treeData': return ChatResponseFilesPart.to(part); + case 'command': return ChatResponseCommandButtonPart.to(part, commandsConverter); } return undefined; } } -export namespace ChatResponseProgress { - export function from(extension: IExtensionDescription, progress: vscode.ChatExtendedProgress): extHostProtocol.IChatProgressDto | undefined { - if ('markdownContent' in progress) { - checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return { content: MarkdownString.from(progress.markdownContent), kind: 'markdownContent' }; - } else if ('content' in progress) { - if ('vulnerabilities' in progress && progress.vulnerabilities) { - checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return { content: progress.content, vulnerabilities: progress.vulnerabilities, kind: 'vulnerability' }; - } - - if (typeof progress.content === 'string') { - return { content: progress.content, kind: 'content' }; - } - - checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return { content: MarkdownString.from(progress.content), kind: 'markdownContent' }; - } else if ('documents' in progress) { - return { - documents: progress.documents.map(d => ({ - uri: d.uri, - version: d.version, - ranges: d.ranges.map(r => Range.from(r)) - })), - kind: 'usedContext' - }; - } else if ('reference' in progress) { - return { - reference: 'uri' in progress.reference ? - { - uri: progress.reference.uri, - range: Range.from(progress.reference.range) - } : progress.reference, - kind: 'reference' - }; - } else if ('inlineReference' in progress) { - return { - inlineReference: 'uri' in progress.inlineReference ? - { - uri: progress.inlineReference.uri, - range: Range.from(progress.inlineReference.range) - } : progress.inlineReference, - name: progress.title, - kind: 'inlineReference' - }; - } else if ('participant' in progress) { - checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return { agentId: progress.participant, command: progress.command, kind: 'agentDetection' }; - } else if ('message' in progress) { - return { content: MarkdownString.from(progress.message), kind: 'progressMessage' }; - } else { - return undefined; - } - } - - export function toProgressContent(progress: extHostProtocol.IChatContentProgressDto, commandsConverter: Command.ICommandsConverter): vscode.ChatContentProgress | undefined { - switch (progress.kind) { - case 'markdownContent': - // For simplicity, don't sent back the 'extended' types, so downgrade markdown to just some text - return { content: progress.content.value }; - case 'inlineReference': - return { - inlineReference: - isUriComponents(progress.inlineReference) ? - URI.revive(progress.inlineReference) : - Location.to(progress.inlineReference), - title: progress.name - }; - case 'command': - // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore - return { - command: commandsConverter.fromInternal(progress.command) ?? { command: progress.command.id, title: progress.command.title }, - }; - default: - // Unknown type, eg something in history that was removed? Ignore - return undefined; - } - } -} - export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined): vscode.ChatRequest { + const requestedTools = request.variables.variables.filter(v => v.isTool).map(tool => tool.id); + const variablesWithoutTools = request.variables.variables.filter(v => !v.isTool); return { prompt: request.message, command: request.command, - variables: request.variables.variables.map(ChatAgentResolvedVariable.to), + attempt: request.attempt ?? 0, + enableCommandDetection: request.enableCommandDetection ?? true, + references: variablesWithoutTools.map(ChatAgentValueReference.to), location: ChatLocation.to(request.location), + acceptedConfirmationData: request.acceptedConfirmationData, + rejectedConfirmationData: request.rejectedConfirmationData, + location2, + requestedTools }; } } @@ -2632,26 +2642,48 @@ export namespace ChatLocation { case ChatAgentLocation.Editor: return types.ChatLocation.Editor; } } + + export function from(loc: types.ChatLocation): ChatAgentLocation { + switch (loc) { + case types.ChatLocation.Notebook: return ChatAgentLocation.Notebook; + case types.ChatLocation.Terminal: return ChatAgentLocation.Terminal; + case types.ChatLocation.Panel: return ChatAgentLocation.Panel; + case types.ChatLocation.Editor: return ChatAgentLocation.Editor; + } + } } -export namespace ChatAgentResolvedVariable { - export function to(request: IChatRequestVariableEntry): vscode.ChatResolvedVariable { +export namespace ChatAgentValueReference { + export function to(variable: IChatRequestVariableEntry): vscode.ChatPromptReference { + const value = variable.value; + if (!value) { + throw new Error('Invalid value reference'); + } + return { - name: request.name, - range: request.range && [request.range.start, request.range.endExclusive], - values: request.values.map(ChatVariable.to) + id: variable.id, + name: variable.name, + range: variable.range && [variable.range.start, variable.range.endExclusive], + value: isUriComponents(value) ? URI.revive(value) : + value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ? + Location.to(revive(value)) : value, + modelDescription: variable.modelDescription }; } } export namespace ChatAgentCompletionItem { - export function from(item: vscode.ChatCompletionItem): extHostProtocol.IChatAgentCompletionItem { + export function from(item: vscode.ChatCompletionItem, commandsConverter: CommandsConverter, disposables: DisposableStore): extHostProtocol.IChatAgentCompletionItem { return { + id: item.id, label: item.label, - values: item.values.map(ChatVariable.from), + fullName: item.fullName, + icon: item.icon?.id, + value: item.values[0].value, insertText: item.insertText, detail: item.detail, documentation: item.documentation, + command: commandsConverter.toInternal(item.command, disposables), }; } } @@ -2661,6 +2693,7 @@ export namespace ChatAgentResult { return { errorDetails: result.errorDetails, metadata: result.metadata, + nextQuestion: result.nextQuestion, }; } } @@ -2674,17 +2707,41 @@ export namespace ChatAgentUserActionEvent { const ehResult = ChatAgentResult.to(result); if (event.action.kind === 'command') { - const commandAction: vscode.ChatCommandAction = { kind: 'command', commandButton: ChatResponseProgress.toProgressContent(event.action.commandButton, commandsConverter) as vscode.ChatCommandButton }; + const command = event.action.commandButton.command; + const commandButton = { + command: commandsConverter.fromInternal(command) ?? { command: command.id, title: command.title }, + }; + const commandAction: vscode.ChatCommandAction = { kind: 'command', commandButton }; return { action: commandAction, result: ehResult }; } else if (event.action.kind === 'followUp') { const followupAction: vscode.ChatFollowupAction = { kind: 'followUp', followup: ChatFollowup.to(event.action.followup) }; return { action: followupAction, result: ehResult }; + } else if (event.action.kind === 'inlineChat') { + return { action: { kind: 'editor', accepted: event.action.action === 'accepted' }, result: ehResult }; } else { return { action: event.action, result: ehResult }; } } } +export namespace LanguageModelToolResult { + export function from(result: vscode.LanguageModelToolResult): IToolResult { + return { + ...result, + string: result.toString(), + }; + } + + export function to(result: IToolResult): vscode.LanguageModelToolResult { + const copy: vscode.LanguageModelToolResult = { + ...result, + toString: () => result.string, + }; + delete copy.string; + + return copy; + } +} export namespace TerminalQuickFix { export function from(quickFix: vscode.TerminalQuickFixTerminalCommand | vscode.TerminalQuickFixOpener | vscode.Command, converter: Command.ICommandsConverter, disposables: DisposableStore): extHostProtocol.ITerminalQuickFixTerminalCommandDto | extHostProtocol.ITerminalQuickFixOpenerDto | extHostProtocol.ICommandDto | undefined { @@ -2733,3 +2790,13 @@ export namespace DebugTreeItem { }; } } + +export namespace LanguageModelToolDescription { + export function to(item: IToolData): vscode.LanguageModelToolDescription { + return { + name: item.name, + description: item.description, + parametersSchema: item.parametersSchema, + }; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index e8faf7df453..f01e28b5413 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -271,6 +271,10 @@ export class Position { toJSON(): any { return { line: this.line, character: this.character }; } + + [Symbol.for('debug.description')]() { + return `(${this.line}:${this.character})`; + } } @es5ClassCompat @@ -417,6 +421,10 @@ export class Range { toJSON(): any { return [this.start, this.end]; } + + [Symbol.for('debug.description')]() { + return getDebugDescriptionOfRange(this); + } } @es5ClassCompat @@ -483,6 +491,29 @@ export class Selection extends Range { anchor: this.anchor }; } + + + [Symbol.for('debug.description')]() { + return getDebugDescriptionOfSelection(this); + } +} + +export function getDebugDescriptionOfRange(range: vscode.Range): string { + return range.isEmpty + ? `[${range.start.line}:${range.start.character})` + : `[${range.start.line}:${range.start.character} -> ${range.end.line}:${range.end.character})`; +} + +export function getDebugDescriptionOfSelection(selection: vscode.Selection): string { + let rangeStr = getDebugDescriptionOfRange(selection); + if (!selection.isEmpty) { + if (selection.active.isEqual(selection.start)) { + rangeStr = `|${rangeStr}`; + } else { + rangeStr = `${rangeStr}|`; + } + } + return rangeStr; } const validateConnectionToken = (connectionToken: string) => { @@ -1202,6 +1233,29 @@ export class Hover { } } +@es5ClassCompat +export class VerboseHover extends Hover { + + public canIncreaseVerbosity: boolean | undefined; + public canDecreaseVerbosity: boolean | undefined; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | (vscode.MarkdownString | vscode.MarkedString)[], + range?: Range, + canIncreaseVerbosity?: boolean, + canDecreaseVerbosity?: boolean, + ) { + super(contents, range); + this.canIncreaseVerbosity = canIncreaseVerbosity; + this.canDecreaseVerbosity = canDecreaseVerbosity; + } +} + +export enum HoverVerbosityAction { + Increase = 0, + Decrease = 1 +} + export enum DocumentHighlightKind { Text = 0, Read = 1, @@ -2001,6 +2055,12 @@ export enum TerminalExitReason { Extension = 4 } +export enum TerminalShellExecutionCommandLineConfidence { + Low = 0, + Medium = 1, + High = 2 +} + export class TerminalLink implements vscode.TerminalLink { constructor( public startIndex: number, @@ -2794,9 +2854,9 @@ export class DocumentDropEdit { additionalEdit?: WorkspaceEdit; - kind?: DocumentPasteEditKind; + kind?: DocumentDropOrPasteEditKind; - constructor(insertText: string | SnippetString, title?: string, kind?: DocumentPasteEditKind) { + constructor(insertText: string | SnippetString, title?: string, kind?: DocumentDropOrPasteEditKind) { this.insertText = insertText; this.title = title; this.kind = kind; @@ -2808,8 +2868,8 @@ export enum DocumentPasteTriggerKind { PasteAs = 1, } -export class DocumentPasteEditKind { - static Empty: DocumentPasteEditKind; +export class DocumentDropOrPasteEditKind { + static Empty: DocumentDropOrPasteEditKind; private static sep = '.'; @@ -2817,29 +2877,28 @@ export class DocumentPasteEditKind { public readonly value: string ) { } - public append(...parts: string[]): DocumentPasteEditKind { - return new DocumentPasteEditKind((this.value ? [this.value, ...parts] : parts).join(DocumentPasteEditKind.sep)); + public append(...parts: string[]): DocumentDropOrPasteEditKind { + return new DocumentDropOrPasteEditKind((this.value ? [this.value, ...parts] : parts).join(DocumentDropOrPasteEditKind.sep)); } - public intersects(other: DocumentPasteEditKind): boolean { + public intersects(other: DocumentDropOrPasteEditKind): boolean { return this.contains(other) || other.contains(this); } - public contains(other: DocumentPasteEditKind): boolean { - return this.value === other.value || other.value.startsWith(this.value + DocumentPasteEditKind.sep); + public contains(other: DocumentDropOrPasteEditKind): boolean { + return this.value === other.value || other.value.startsWith(this.value + DocumentDropOrPasteEditKind.sep); } } -DocumentPasteEditKind.Empty = new DocumentPasteEditKind(''); +DocumentDropOrPasteEditKind.Empty = new DocumentDropOrPasteEditKind(''); -@es5ClassCompat export class DocumentPasteEdit { title: string; insertText: string | SnippetString; additionalEdit?: WorkspaceEdit; - kind: DocumentPasteEditKind; + kind: DocumentDropOrPasteEditKind; - constructor(insertText: string | SnippetString, title: string, kind: DocumentPasteEditKind) { + constructor(insertText: string | SnippetString, title: string, kind: DocumentDropOrPasteEditKind) { this.title = title; this.insertText = insertText; this.kind = kind; @@ -3071,14 +3130,14 @@ export class DebugAdapterInlineImplementation implements vscode.DebugAdapterInli } -export class StackFrame implements vscode.StackFrame { +export class DebugStackFrame implements vscode.DebugStackFrame { constructor( public readonly session: vscode.DebugSession, readonly threadId: number, readonly frameId: number) { } } -export class Thread implements vscode.Thread { +export class DebugThread implements vscode.DebugThread { constructor( public readonly session: vscode.DebugSession, readonly threadId: number) { } @@ -3152,6 +3211,11 @@ export enum NewSymbolNameTag { AIGenerated = 1 } +export enum NewSymbolNameTriggerKind { + Invoke = 0, + Automatic = 1, +} + export class NewSymbolName implements vscode.NewSymbolName { readonly newSymbolName: string; readonly tags?: readonly vscode.NewSymbolNameTag[] | undefined; @@ -3277,6 +3341,11 @@ export enum CommentThreadApplicability { Outdated = 1 } +export enum CommentThreadFocus { + Reply = 1, + Comment = 2 +} + //#endregion //#region Semantic Coloring @@ -3528,6 +3597,11 @@ export class DebugVisualization { //#endregion +export enum QuickInputButtonLocation { + Title = 1, + Inline = 2 +} + @es5ClassCompat export class QuickInputButtons { @@ -3996,6 +4070,7 @@ export class TestRunRequest implements vscode.TestRunRequest { public readonly exclude: vscode.TestItem[] | undefined = undefined, public readonly profile: vscode.TestRunProfile | undefined = undefined, public readonly continuous = false, + public readonly preserveFocus = true, ) { } } @@ -4004,9 +4079,11 @@ export class TestMessage implements vscode.TestMessage { public expectedOutput?: string; public actualOutput?: string; public location?: vscode.Location; - /** proposed: */ public contextValue?: string; + /** proposed: */ + public stackTrace?: TestMessageStackFrame[]; + public static diff(message: string | vscode.MarkdownString, expected: string, actual: string) { const msg = new TestMessage(message); msg.expectedOutput = expected; @@ -4022,6 +4099,19 @@ export class TestTag implements vscode.TestTag { constructor(public readonly id: string) { } } +export class TestMessageStackFrame { + /** + * @param label The name of the stack frame + * @param file The file URI of the stack frame + * @param position The position of the stack frame within the file + */ + constructor( + public label: string, + public uri?: vscode.Uri, + public position?: Position, + ) { } +} + //#endregion //#region Test Coverage @@ -4085,6 +4175,7 @@ export class FileCoverage implements vscode.FileCoverage { public statementCoverage: vscode.TestCoverageCount, public branchCoverage?: vscode.TestCoverageCount, public declarationCoverage?: vscode.TestCoverageCount, + public fromTests: vscode.TestItem[] = [], ) { } } @@ -4208,7 +4299,7 @@ export class InteractiveWindowInput { } export class ChatEditorTabInput { - constructor(readonly providerId: string) { } + constructor() { } } export class TextMultiDiffTabInput { @@ -4235,13 +4326,18 @@ export enum ChatVariableLevel { } export class ChatCompletionItem implements vscode.ChatCompletionItem { + id: string; label: string | CompletionItemLabel; + fullName?: string | undefined; + icon?: vscode.ThemeIcon; insertText?: string; values: vscode.ChatVariableValue[]; detail?: string; documentation?: string | MarkdownString; + command?: vscode.Command; - constructor(label: string | CompletionItemLabel, values: vscode.ChatVariableValue[]) { + constructor(id: string, label: string | CompletionItemLabel, values: vscode.ChatVariableValue[]) { + this.id = id; this.label = label; this.values = values; } @@ -4267,10 +4363,55 @@ export enum ChatResultFeedbackKind { export class ChatResponseMarkdownPart { value: vscode.MarkdownString; constructor(value: string | vscode.MarkdownString) { + if (typeof value !== 'string' && value.isTrusted === true) { + throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.'); + } + this.value = typeof value === 'string' ? new MarkdownString(value) : value; } } +/** + * TODO if 'vulnerabilities' is finalized, this should be merged with the base ChatResponseMarkdownPart. I just don't see how to do that while keeping + * vulnerabilities in a seperate API proposal in a clean way. + */ +export class ChatResponseMarkdownWithVulnerabilitiesPart { + value: vscode.MarkdownString; + vulnerabilities: vscode.ChatVulnerability[]; + constructor(value: string | vscode.MarkdownString, vulnerabilities: vscode.ChatVulnerability[]) { + if (typeof value !== 'string' && value.isTrusted === true) { + throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.'); + } + + this.value = typeof value === 'string' ? new MarkdownString(value) : value; + this.vulnerabilities = vulnerabilities; + } +} + +export class ChatResponseDetectedParticipantPart { + participant: string; + // TODO@API validate this against statically-declared slash commands? + command?: vscode.ChatCommand; + constructor(participant: string, command?: vscode.ChatCommand) { + this.participant = participant; + this.command = command; + } +} + +export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + buttons?: string[]; + + constructor(title: string, message: string, data: any, buttons?: string[]) { + this.title = title; + this.message = message; + this.data = data; + this.buttons = buttons; + } +} + export class ChatResponseFileTreePart { value: vscode.ChatResponseFileTree[]; baseUri: vscode.Uri; @@ -4281,9 +4422,9 @@ export class ChatResponseFileTreePart { } export class ChatResponseAnchorPart { - value: vscode.Uri | vscode.Location | vscode.SymbolInformation; + value: vscode.Uri | vscode.Location; title?: string; - constructor(value: vscode.Uri | vscode.Location | vscode.SymbolInformation, title?: string) { + constructor(value: vscode.Uri | vscode.Location, title?: string) { this.value = value; this.title = title; } @@ -4296,6 +4437,26 @@ export class ChatResponseProgressPart { } } +export class ChatResponseProgressPart2 { + value: string; + task?: (progress: vscode.Progress) => Thenable; + constructor(value: string, task?: (progress: vscode.Progress) => Thenable) { + this.value = value; + this.task = task; + } +} + +export class ChatResponseWarningPart { + value: vscode.MarkdownString; + constructor(value: string | vscode.MarkdownString) { + if (typeof value !== 'string' && value.isTrusted === true) { + throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.'); + } + + this.value = typeof value === 'string' ? new MarkdownString(value) : value; + } +} + export class ChatResponseCommandButtonPart { value: vscode.Command; constructor(value: vscode.Command) { @@ -4305,17 +4466,40 @@ export class ChatResponseCommandButtonPart { export class ChatResponseReferencePart { value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }; - constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }) { + iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; + options?: { status?: { description: string; kind: vscode.ChatResponseReferencePartStatusKind } }; + constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }, options?: { status?: { description: string; kind: vscode.ChatResponseReferencePartStatusKind } }) { this.value = value; + this.iconPath = iconPath; + this.options = options; } } +export class ChatResponseCodeCitationPart { + value: vscode.Uri; + license: string; + snippet: string; + constructor(value: vscode.Uri, license: string, snippet: string) { + this.value = value; + this.license = license; + this.snippet = snippet; + } +} + +export class ChatResponseTextEditPart { + uri: vscode.Uri; + edits: vscode.TextEdit[]; + constructor(uri: vscode.Uri, edits: vscode.TextEdit | vscode.TextEdit[]) { + this.uri = uri; + this.edits = Array.isArray(edits) ? edits : [edits]; + } +} export class ChatRequestTurn implements vscode.ChatRequestTurn { constructor( readonly prompt: string, readonly command: string | undefined, - readonly variables: vscode.ChatResolvedVariable[], + readonly references: vscode.ChatPromptReference[], readonly participant: string, ) { } } @@ -4337,14 +4521,103 @@ export enum ChatLocation { Editor = 4, } +export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 +} + +export class ChatRequestEditorData implements vscode.ChatRequestEditorData { + constructor( + readonly document: vscode.TextDocument, + readonly selection: vscode.Selection, + readonly wholeRange: vscode.Range, + ) { } +} + +export class ChatRequestNotebookData implements vscode.ChatRequestNotebookData { + constructor( + readonly cell: vscode.TextDocument + ) { } +} + +export enum LanguageModelChatMessageRole { + User = 1, + Assistant = 2, + System = 3 +} + +export class LanguageModelFunctionResultPart implements vscode.LanguageModelChatMessageFunctionResultPart { + + name: string; + content: string; + isError: boolean; + + constructor(name: string, content: string, isError?: boolean) { + this.name = name; + this.content = content; + this.isError = isError ?? false; + } +} + +export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage { + + static User(content: string | LanguageModelFunctionResultPart, name?: string): LanguageModelChatMessage { + const value = new LanguageModelChatMessage(LanguageModelChatMessageRole.User, typeof content === 'string' ? content : '', name); + value.content2 = content; + return value; + } + + static Assistant(content: string, name?: string): LanguageModelChatMessage { + return new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, content, name); + } + + role: vscode.LanguageModelChatMessageRole; + content: string; + content2: string | vscode.LanguageModelChatMessageFunctionResultPart; + name: string | undefined; + + constructor(role: vscode.LanguageModelChatMessageRole, content: string, name?: string) { + this.role = role; + this.content = content; + this.content2 = content; + this.name = name; + } +} + +export class LanguageModelFunctionUsePart implements vscode.LanguageModelChatResponseFunctionUsePart { + name: string; + parameters: any; + + constructor(name: string, parameters: any) { + this.name = name; + this.parameters = parameters; + } +} + +export class LanguageModelTextPart implements vscode.LanguageModelChatResponseTextPart { + value: string; + + constructor(value: string) { + this.value = value; + + } +} + +/** + * @deprecated + */ export class LanguageModelChatSystemMessage { content: string; - constructor(content: string) { this.content = content; } } + +/** + * @deprecated + */ export class LanguageModelChatUserMessage { content: string; name: string | undefined; @@ -4355,6 +4628,9 @@ export class LanguageModelChatUserMessage { } } +/** + * @deprecated + */ export class LanguageModelChatAssistantMessage { content: string; name?: string; @@ -4375,6 +4651,10 @@ export class LanguageModelError extends Error { return new LanguageModelError(message, LanguageModelError.NoPermissions.name); } + static Blocked(message?: string): LanguageModelError { + return new LanguageModelError(message, LanguageModelError.Blocked.name); + } + readonly code: string; constructor(message?: string, code?: string, cause?: Error) { @@ -4404,7 +4684,14 @@ export enum SpeechToTextStatus { Started = 1, Recognizing = 2, Recognized = 3, - Stopped = 4 + Stopped = 4, + Error = 5 +} + +export enum TextToSpeechStatus { + Started = 1, + Stopped = 2, + Error = 3 } export enum KeywordRecognitionStatus { diff --git a/src/vs/workbench/api/common/extHostWebviewMessaging.ts b/src/vs/workbench/api/common/extHostWebviewMessaging.ts index e079f66e9e4..88c477baab5 100644 --- a/src/vs/workbench/api/common/extHostWebviewMessaging.ts +++ b/src/vs/workbench/api/common/extHostWebviewMessaging.ts @@ -30,15 +30,15 @@ export function serializeWebviewMessage( const replacer = (_key: string, value: any) => { if (value instanceof ArrayBuffer) { const index = arrayBuffers.add(value); - return { + return { $$vscode_array_buffer_reference$$: true, index, - }; + } satisfies extHostProtocol.WebviewMessageArrayBufferReference; } else if (ArrayBuffer.isView(value)) { const type = getTypedArrayType(value); if (type) { const index = arrayBuffers.add(value.buffer); - return { + return { $$vscode_array_buffer_reference$$: true, index, view: { @@ -46,7 +46,7 @@ export function serializeWebviewMessage( byteLength: value.byteLength, byteOffset: value.byteOffset, } - }; + } satisfies extHostProtocol.WebviewMessageArrayBufferReference; } } diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index 3d32449bbba..82b721fe489 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -3,16 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostWindowShape, MainContext, MainThreadWindowShape, IOpenUriOptions } from './extHost.protocol'; -import { WindowState } from 'vscode'; -import { URI } from 'vs/base/common/uri'; +import { Emitter, Event } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { WindowState } from 'vscode'; +import { ExtHostWindowShape, IOpenUriOptions, MainContext, MainThreadWindowShape } from './extHost.protocol'; export class ExtHostWindow implements ExtHostWindowShape { @@ -28,7 +26,7 @@ export class ExtHostWindow implements ExtHostWindowShape { private _state = ExtHostWindow.InitialState; - getState(extension: Readonly): WindowState { + getState(): WindowState { // todo@connor4312: this can be changed to just return this._state after proposed api is finalized const state = this._state; @@ -37,7 +35,6 @@ export class ExtHostWindow implements ExtHostWindowShape { return state.focused; }, get active() { - checkProposedApiEnabled(extension, 'windowActivity'); return state.active; }, }; diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index d240127a4d4..a25ff3fe913 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -33,12 +33,15 @@ import { IRawFileMatch2, ITextSearchResult, resultIsMatch } from 'vs/workbench/s import * as vscode from 'vscode'; import { ExtHostWorkspaceShape, IRelativePatternDto, IWorkspaceData, MainContext, MainThreadMessageOptions, MainThreadMessageServiceShape, MainThreadWorkspaceShape } from './extHost.protocol'; import { revive } from 'vs/base/common/marshalling'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; export interface IExtHostWorkspaceProvider { getWorkspaceFolder2(uri: vscode.Uri, resolveParent?: boolean): Promise; resolveWorkspaceFolder(uri: vscode.Uri): Promise; getWorkspaceFolders2(): Promise; resolveProxy(url: string): Promise; + lookupAuthorization(authInfo: AuthInfo): Promise; + lookupKerberosAuthorization(url: string): Promise; loadCertificates(): Promise; } @@ -132,7 +135,7 @@ class ExtHostWorkspaceImpl extends Workspace { constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[], transient: boolean, configuration: URI | null, private _isUntitled: boolean, ignorePathCasing: (key: URI) => boolean) { super(id, folders.map(f => new WorkspaceFolder(f)), transient, configuration, ignorePathCasing); - this._structure = TernarySearchTree.forUris(ignorePathCasing); + this._structure = TernarySearchTree.forUris(ignorePathCasing, () => true); // setup the workspace folder data structure folders.forEach(folder => { @@ -418,7 +421,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac configuration: this._actualWorkspace.configuration, folders, isUntitled: this._actualWorkspace.isUntitled - } as IWorkspaceData, this._actualWorkspace, undefined, this._extHostFileSystemInfo).workspace || undefined; + }, this._actualWorkspace, undefined, this._extHostFileSystemInfo).workspace || undefined; } } @@ -488,7 +491,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac const excludePattern = (typeof options.exclude === 'string') ? options.exclude : options.exclude ? options.exclude.pattern : undefined; - const fileQueries = { + const fileQueries: IFileQueryBuilderOptions = { ignoreSymlinks: typeof options.followSymlinks === 'boolean' ? !options.followSymlinks : undefined, disregardIgnoreFiles: typeof options.useIgnoreFiles === 'boolean' ? !options.useIgnoreFiles : undefined, disregardGlobalIgnoreFiles: typeof options.useGlobalIgnoreFiles === 'boolean' ? !options.useGlobalIgnoreFiles : undefined, @@ -519,7 +522,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac .then(data => Array.isArray(data) ? data.map(d => URI.revive(d)) : []); } - async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise { + async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions & { useSearchExclude?: boolean }, callback: (result: vscode.TextSearchResult) => void, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise { this._logService.trace(`extHostWorkspace#findTextInFiles: textSearch, extension: ${extensionId.value}, entryPoint: findTextInFiles`); const requestId = this._requestIdProvider.getNext(); @@ -540,6 +543,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac disregardGlobalIgnoreFiles: typeof options.useGlobalIgnoreFiles === 'boolean' ? !options.useGlobalIgnoreFiles : undefined, disregardParentIgnoreFiles: typeof options.useParentIgnoreFiles === 'boolean' ? !options.useParentIgnoreFiles : undefined, disregardExcludeSettings: typeof options.useDefaultExcludes === 'boolean' ? !options.useDefaultExcludes : true, + disregardSearchExcludeSettings: typeof options.useSearchExclude === 'boolean' ? !options.useSearchExclude : true, fileEncoding: options.encoding, maxResults: options.maxResults, previewOptions, @@ -561,7 +565,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac p.results!.forEach(rawResult => { const result: ITextSearchResult = revive(rawResult); if (resultIsMatch(result)) { - callback({ + callback({ uri, preview: { text: result.preview.text, @@ -572,13 +576,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac ranges: mapArrayOrNot( result.ranges, r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn)) - }); + } satisfies vscode.TextSearchMatch); } else { - callback({ + callback({ uri, text: result.text, lineNumber: result.lineNumber - }); + } satisfies vscode.TextSearchContext); } }); }; @@ -626,6 +630,14 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return this._proxy.$resolveProxy(url); } + lookupAuthorization(authInfo: AuthInfo): Promise { + return this._proxy.$lookupAuthorization(authInfo); + } + + lookupKerberosAuthorization(url: string): Promise { + return this._proxy.$lookupKerberosAuthorization(url); + } + loadCertificates(): Promise { return this._proxy.$loadCertificates(); } diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 2bd275cbc21..50c47ba98a8 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -11,7 +11,7 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -195,7 +195,7 @@ export class ExtensionHostMain { private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { initData.extensions.allExtensions.forEach((ext) => { - (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); + (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); }); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); const extDevLocs = initData.environment.extensionDevelopmentLocationURI; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 995d4f8a17b..dc48354e372 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { createCancelablePromise, firstParallel, timeout } from 'vs/base/common/async'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import * as nls from 'vs/nls'; @@ -27,7 +27,7 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import type * as vscode from 'vscode'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { createHash } from 'crypto'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -45,8 +45,9 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, @IExtHostCommands commands: IExtHostCommands, + @IExtHostTesting testing: IExtHostTesting, ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -79,9 +80,9 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { if (!this._terminalDisposedListener) { // React on terminal disposed and check if that is the debug terminal #12956 - this._terminalDisposedListener = this._terminalService.onDidCloseTerminal(terminal => { + this._terminalDisposedListener = this._register(this._terminalService.onDidCloseTerminal(terminal => { this._integratedTerminalInstances.onTerminalClosed(terminal); - }); + })); } const configProvider = await this._configurationService.getConfigProvider(); @@ -90,8 +91,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { const terminalName = args.title || nls.localize('debug.terminal.title', "Debug Process"); - const termKey = createKeyForShell(shell, shellArgs, args); - let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName, true); + const shellConfig = JSON.stringify({ shell, shellArgs }); + let terminal = await this._integratedTerminalInstances.checkout(shellConfig, terminalName); let cwdForPrepareCommand: string | undefined; let giveShellTimeToInitialize = false; @@ -103,7 +104,6 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { cwd: args.cwd, name: terminalName, iconPath: new ThemeIcon('debug'), - env: args.env, }; giveShellTimeToInitialize = true; terminal = this._terminalService.createTerminalFromOptions(options, { @@ -113,7 +113,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { forceShellIntegration: true, useShellEnvironment: true }); - this._integratedTerminalInstances.insert(terminal, termKey); + this._integratedTerminalInstances.insert(terminal, shellConfig); } else { cwdForPrepareCommand = args.cwd; @@ -129,6 +129,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } else { if (terminal.state.isInteractedWith) { terminal.sendText('\u0003'); // Ctrl+C for #106743. Not part of the same command for #107969 + await timeout(200); // mirroring https://github.com/microsoft/vscode/blob/c67ccc70ece5f472ec25464d3eeb874cfccee9f1/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts#L852-L857 } if (configProvider.getConfiguration('debug.terminal').get('clearBeforeReusing')) { @@ -145,7 +146,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } } - const command = prepareCommand(shell, args.args, !!args.argsCanBeInterpretedByShell, cwdForPrepareCommand); + const command = prepareCommand(shell, args.args, !!args.argsCanBeInterpretedByShell, cwdForPrepareCommand, args.env); terminal.sendText(command); // Mark terminal as unused when its session ends, see #112055 @@ -165,14 +166,6 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } } -/** Creates a key that determines how terminals get reused */ -function createKeyForShell(shell: string, shellArgs: string | string[], args: DebugProtocol.RunInTerminalRequestArguments) { - const hash = createHash('sha256'); - hash.update(JSON.stringify({ shell, shellArgs })); - hash.update(JSON.stringify(Object.entries(args.env || {}).sort(([k1], [k2]) => k1.localeCompare(k2)))); - return hash.digest('base64'); -} - let externalTerminalService: IExternalTerminalService | undefined = undefined; function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 5ac0ddc7ec9..ec6bb2bd1f4 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -18,6 +18,10 @@ import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { realpathSync } from 'vs/base/node/extpath'; import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder'; import { ExtHostDiskFileSystemProvider } from 'vs/workbench/api/node/extHostDiskFileSystemProvider'; +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// const require = createRequire(import.meta.url); +// ESM-uncomment-end class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -109,7 +113,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { if (extensionId) { performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`); } - r = require.__$__nodeRequire(module.fsPath); + r = require.__$__nodeRequire(module.fsPath); } finally { if (extensionId) { performance.mark(`code/extHost/didLoadExtensionCode/${extensionId}`); diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 99f5d7614e8..d10dbbb7d99 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -8,6 +8,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; +import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostSearch, reviveQuery } from 'vs/workbench/api/common/extHostSearch'; @@ -29,24 +30,59 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { private _registeredEHSearchProvider = false; + private _numThreadsPromise: Promise | undefined; + private readonly _disposables = new DisposableStore(); + private isDisposed = false; + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @IURITransformerService _uriTransformer: IURITransformerService, + @IExtHostConfiguration private readonly configurationService: IExtHostConfiguration, @ILogService _logService: ILogService, ) { super(extHostRpc, _uriTransformer, _logService); - + this.getNumThreads = this.getNumThreads.bind(this); + this.getNumThreadsCached = this.getNumThreadsCached.bind(this); + this.handleConfigurationChanged = this.handleConfigurationChanged.bind(this); const outputChannel = new OutputChannel('RipgrepSearchUD', this._logService); - this._disposables.add(this.registerTextSearchProvider(Schemas.vscodeUserData, new RipgrepSearchProvider(outputChannel))); + this._disposables.add(this.registerTextSearchProvider(Schemas.vscodeUserData, new RipgrepSearchProvider(outputChannel, this.getNumThreadsCached))); if (initData.remote.isRemote && initData.remote.authority) { this._registerEHSearchProviders(); } + + configurationService.getConfigProvider().then(provider => { + if (this.isDisposed) { + return; + } + this._disposables.add(provider.onDidChangeConfiguration(this.handleConfigurationChanged)); + }); + } + + private handleConfigurationChanged(event: vscode.ConfigurationChangeEvent) { + if (!event.affectsConfiguration('search')) { + return; + } + this._numThreadsPromise = undefined; + } + + async getNumThreads(): Promise { + const configProvider = await this.configurationService.getConfigProvider(); + const numThreads = configProvider.getConfiguration('search').get('ripgrep.maxThreads'); + return numThreads; + } + + async getNumThreadsCached(): Promise { + if (!this._numThreadsPromise) { + this._numThreadsPromise = this.getNumThreads(); + } + return this._numThreadsPromise; } dispose(): void { + this.isDisposed = true; this._disposables.dispose(); } @@ -61,8 +97,8 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { this._registeredEHSearchProvider = true; const outputChannel = new OutputChannel('RipgrepSearchEH', this._logService); - this._disposables.add(this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel))); - this._disposables.add(this.registerInternalFileSearchProvider(Schemas.file, new SearchService('fileSearchProvider'))); + this._disposables.add(this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel, this.getNumThreadsCached))); + this._disposables.add(this.registerInternalFileSearchProvider(Schemas.file, new SearchService('fileSearchProvider', this.getNumThreadsCached))); } private registerInternalFileSearchProvider(scheme: string, provider: SearchService): IDisposable { @@ -90,7 +126,7 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { return super.$provideFileSearchResults(handle, session, rawQuery, token); } - override doInternalFileSearchWithCustomCallback(rawQuery: IFileQuery, token: vscode.CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { + override async doInternalFileSearchWithCustomCallback(rawQuery: IFileQuery, token: vscode.CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { const onResult = (ev: ISerializedSearchProgressItem) => { if (isSerializedFileMatch(ev)) { ev = [ev]; @@ -109,8 +145,8 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { if (!this._internalFileSearchProvider) { throw new Error('No internal file search handler'); } - - return >this._internalFileSearchProvider.doFileSearch(rawQuery, onResult, token); + const numThreads = await this.getNumThreadsCached(); + return >this._internalFileSearchProvider.doFileSearch(rawQuery, numThreads, onResult, token); } private async doInternalFileSearch(handle: number, session: number, rawQuery: IFileQuery, token: vscode.CancellationToken): Promise { diff --git a/src/vs/workbench/api/node/extHostStoragePaths.ts b/src/vs/workbench/api/node/extHostStoragePaths.ts index 8348d9490b2..e259b3b0d6d 100644 --- a/src/vs/workbench/api/node/extHostStoragePaths.ts +++ b/src/vs/workbench/api/node/extHostStoragePaths.ts @@ -69,14 +69,14 @@ export class ExtensionStoragePaths extends CommonExtensionStoragePaths { async function mkdir(dir: string): Promise { try { - await Promises.stat(dir); + await fs.promises.stat(dir); return; } catch { // doesn't exist, that's OK } try { - await Promises.mkdir(dir, { recursive: true }); + await fs.promises.mkdir(dir, { recursive: true }); } catch { } } @@ -103,7 +103,7 @@ class Lock extends Disposable { this._timer.cancel(); } try { - await Promises.utimes(filename, new Date(), new Date()); + await fs.promises.utimes(filename, new Date(), new Date()); } catch (err) { logService.error(err); logService.info(`Lock '${filename}': Could not update mtime.`); @@ -174,7 +174,7 @@ interface ILockfileContents { async function readLockfileContents(logService: ILogService, filename: string): Promise { let contents: Buffer; try { - contents = await Promises.readFile(filename); + contents = await fs.promises.readFile(filename); } catch (err) { // cannot read the file logService.error(err); @@ -196,7 +196,7 @@ async function readLockfileContents(logService: ILogService, filename: string): async function readmtime(logService: ILogService, filename: string): Promise { let stats: fs.Stats; try { - stats = await Promises.stat(filename); + stats = await fs.promises.stat(filename); } catch (err) { // cannot read the file stats to check if it is stale or not logService.error(err); @@ -279,7 +279,7 @@ async function checkStaleAndTryAcquireLock(logService: ILogService, filename: st async function tryDeleteAndAcquireLock(logService: ILogService, filename: string): Promise { logService.info(`Lock '${filename}': Deleting a stale lock.`); try { - await Promises.unlink(filename); + await fs.promises.unlink(filename); } catch (err) { // cannot delete the file // maybe the file is already deleted diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 2ab734a61a6..54b83109489 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { exec } from 'child_process'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; @@ -37,10 +38,10 @@ export function getSockets(stdout: string): Record { + const socketMap = mapped.reduce((m: Record, socket) => { m[socket.socket] = socket; return m; - }, {} as Record); + }, {}); return socketMap; } @@ -96,10 +97,10 @@ export function loadConnectionTable(stdout: string): Record[] { const lines = stdout.trim().split('\n'); const names = lines.shift()!.trim().split(/\s+/) .filter(name => name !== 'rx_queue' && name !== 'tm->when'); - const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { + const table = lines.map(line => line.trim().split(/\s+/).reduce((obj: Record, value, i) => { obj[names[i] || i] = value; return obj; - }, {} as Record)); + }, {})); return table; } @@ -126,10 +127,10 @@ export function getRootProcesses(stdout: string) { } export async function findPorts(connections: { socket: number; ip: string; port: number }[], socketMap: Record, processes: { pid: number; cwd: string; cmd: string }[]): Promise { - const processMap = processes.reduce((m, process) => { + const processMap = processes.reduce((m: Record, process) => { m[process.pid] = process; return m; - }, {} as Record); + }, {}); const ports: CandidatePort[] = []; connections.forEach(({ socket, ip, port }) => { @@ -243,8 +244,8 @@ export class NodeExtHostTunnelService extends ExtHostTunnelService { let tcp: string = ''; let tcp6: string = ''; try { - tcp = await pfs.Promises.readFile('/proc/net/tcp', 'utf8'); - tcp6 = await pfs.Promises.readFile('/proc/net/tcp6', 'utf8'); + tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -265,10 +266,10 @@ export class NodeExtHostTunnelService extends ExtHostTunnelService { try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = await pfs.Promises.stat(childUri.fsPath); + const childStat = await fs.promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = await pfs.Promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = await pfs.Promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index 785db7edd43..80a60c1d4c1 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -3,29 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import minimist from 'minimist'; import * as nativeWatchdog from 'native-watchdog'; import * as net from 'net'; -import * as minimist from 'minimist'; -import * as performance from 'vs/base/common/performance'; -import type { MessagePortMain } from 'vs/base/parts/sandbox/node/electronTypes'; +import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; import { isCancellationError, isSigPipeError, onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol, ProtocolConstants, BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net'; -import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import product from 'vs/platform/product/common/product'; -import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode, IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostMain, IExitFn } from 'vs/workbench/api/common/extensionHostMain'; -import { VSBuffer } from 'vs/base/common/buffer'; +import * as performance from 'vs/base/common/performance'; import { IURITransformer } from 'vs/base/common/uriIpc'; -import { Promises } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; -import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; -import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; +import { Promises } from 'vs/base/node/pfs'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { BufferedEmitter, PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import type { MessagePortMain } from 'vs/base/parts/sandbox/node/electronTypes'; import { boolean } from 'vs/editor/common/config/editorOptions'; +import product from 'vs/platform/product/common/product'; +import { ExtensionHostMain, IExitFn } from 'vs/workbench/api/common/extensionHostMain'; +import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; import { createURITransformer } from 'vs/workbench/api/node/uriTransformer'; import { ExtHostConnectionType, readExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { ExtensionHostExitCode, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage, IExtensionHostInitData, MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IDisposable } from 'vs/base/common/lifecycle'; import 'vs/workbench/api/common/extHost.common.services'; import 'vs/workbench/api/node/extHost.node.services'; @@ -251,12 +252,14 @@ async function createExtHostProtocol(): Promise { readonly onMessage: Event = this._onMessage.event; private _terminating: boolean; + private _protocolListener: IDisposable; constructor() { this._terminating = false; - protocol.onMessage((msg) => { + this._protocolListener = protocol.onMessage((msg) => { if (isMessageOfType(msg, MessageType.Terminate)) { this._terminating = true; + this._protocolListener.dispose(); onTerminate('received terminate message from renderer'); } else { this._onMessage.fire(msg); diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 519924eec13..e6f9cf77053 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// ESM-comment-begin import * as http from 'http'; import * as https from 'https'; import * as tls from 'tls'; import * as net from 'net'; +// ESM-comment-end import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; @@ -17,6 +19,16 @@ import { URI } from 'vs/base/common/uri'; import { ILogService, LogLevel as LogServiceLevel } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates } from '@vscode/proxy-agent'; +import { AuthInfo } from 'vs/platform/request/common/request'; + +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// const require = createRequire(import.meta.url); +// const http = require('http'); +// const https = require('https'); +// const tls = require('tls'); +// const net = require('net'); +// ESM-uncomment-end const systemCertificatesV2Default = false; @@ -32,9 +44,10 @@ export function connectProxyResolver( const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; const params: ProxyAgentParams = { resolveProxy: url => extHostWorkspace.resolveProxy(url), - lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostLogService, mainThreadTelemetry, configProvider, {}, initData.remote.isRemote), + lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, doUseHostProxy), getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', + getNoProxyConfig: () => configProvider.getConfiguration('http').get('noProxy') || [], addCertificatesV1: () => certSettingV1(configProvider), addCertificatesV2: () => certSettingV2(configProvider), log: extHostLogService, @@ -67,6 +80,11 @@ export function connectProxyResolver( certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length)); promises.push(certs); } + // Using https.globalAgent because it is shared with proxy.test.ts and mutable. + if (initData.environment.extensionTestsLocationURI && (https.globalAgent as any).testCertificates?.length) { + extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading test certificates'); + promises.push(Promise.resolve((https.globalAgent as any).testCertificates as string[])); + } return (await Promise.all(promises)).flat(); }, env: process.env, @@ -77,11 +95,16 @@ export function connectProxyResolver( } function createPatchedModules(params: ProxyAgentParams, resolveProxy: ReturnType) { + + function mergeModules(module: any, patch: any) { + return Object.assign(module.default || module, patch); + } + return { - http: Object.assign(http, createHttpPatch(params, http, resolveProxy)), - https: Object.assign(https, createHttpPatch(params, https, resolveProxy)), - net: Object.assign(net, createNetPatch(params, net)), - tls: Object.assign(tls, createTlsPatch(params, tls)) + http: mergeModules(http, createHttpPatch(params, http, resolveProxy)), + https: mergeModules(https, createHttpPatch(params, https, resolveProxy)), + net: mergeModules(net, createNetPatch(params, net)), + tls: mergeModules(tls, createTlsPatch(params, tls)) }; } @@ -129,14 +152,17 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku } async function lookupProxyAuthorization( + extHostWorkspace: IExtHostWorkspaceProvider, extHostLogService: ILogService, mainThreadTelemetry: MainThreadTelemetryShape, configProvider: ExtHostConfigProvider, proxyAuthenticateCache: Record, + basicAuthCache: Record, isRemote: boolean, + useHostProxy: boolean, proxyURL: string, proxyAuthenticate: string | string[] | undefined, - state: { kerberosRequested?: boolean } + state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number } ): Promise { const cached = proxyAuthenticateCache[proxyURL]; if (proxyAuthenticate) { @@ -147,8 +173,9 @@ async function lookupProxyAuthorization( const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : []; sendTelemetry(mainThreadTelemetry, authenticate, isRemote); if (authenticate.some(a => /^(Negotiate|Kerberos)( |$)/i.test(a)) && !state.kerberosRequested) { + state.kerberosRequested = true; + try { - state.kerberosRequested = true; const kerberos = await import('kerberos'); const url = new URL(proxyURL); const spn = configProvider.getConfiguration('http').get('proxyKerberosServicePrincipal') @@ -158,7 +185,54 @@ async function lookupProxyAuthorization( const response = await client.step(''); return 'Negotiate ' + response; } catch (err) { - extHostLogService.error('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); + } + + if (isRemote && useHostProxy) { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup on host', `proxyURL:${proxyURL}`); + const auth = await extHostWorkspace.lookupKerberosAuthorization(proxyURL); + if (auth) { + return 'Negotiate ' + auth; + } + } + } + const basicAuthHeader = authenticate.find(a => /^Basic( |$)/i.test(a)); + if (basicAuthHeader) { + try { + const cachedAuth = basicAuthCache[proxyURL]; + if (cachedAuth) { + if (state.basicAuthCacheUsed) { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication deleting cached credentials', `proxyURL:${proxyURL}`); + delete basicAuthCache[proxyURL]; + } else { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication using cached credentials', `proxyURL:${proxyURL}`); + state.basicAuthCacheUsed = true; + return cachedAuth; + } + } + state.basicAuthAttempt = (state.basicAuthAttempt || 0) + 1; + const realm = / realm="([^"]+)"/i.exec(basicAuthHeader)?.[1]; + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication lookup', `proxyURL:${proxyURL}`, `realm:${realm}`); + const url = new URL(proxyURL); + const authInfo: AuthInfo = { + scheme: 'basic', + host: url.hostname, + port: Number(url.port), + realm: realm || '', + isProxy: true, + attempt: state.basicAuthAttempt, + }; + const credentials = await extHostWorkspace.lookupAuthorization(authInfo); + if (credentials) { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received credentials', `proxyURL:${proxyURL}`, `realm:${realm}`); + const auth = 'Basic ' + Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); + basicAuthCache[proxyURL] = auth; + return auth; + } else { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received no credentials', `proxyURL:${proxyURL}`, `realm:${realm}`); + } + } catch (err) { + extHostLogService.error('ProxyResolver#lookupProxyAuthorization Basic authentication failed', err); } } return undefined; diff --git a/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts b/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts index 35e7ff35360..d5db3ca4683 100644 --- a/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts +++ b/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { originalFSPath } from 'vs/base/common/resources'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index b49d05ed00f..da5e8bb9e6f 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -17,7 +17,7 @@ import 'vs/editor/contrib/suggest/browser/suggest'; import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/inlayHints/browser/inlayHintsController'; -import * as assert from 'assert'; +import assert from 'assert'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; @@ -1275,6 +1275,22 @@ suite('ExtHostLanguageFeatureCommands', function () { }); + testApiCmd('DocumentLink[] vscode.executeLinkProvider returns lack tooltip #213970', async function () { + disposables.push(extHost.registerDocumentLinkProvider(nullExtensionDescription, defaultSelector, { + provideDocumentLinks(): any { + const link = new types.DocumentLink(new types.Range(0, 0, 0, 20), URI.parse('foo:bar')); + link.tooltip = 'Link Tooltip'; + return [link]; + } + })); + + await rpcProtocol.sync(); + + const links1 = await commands.executeCommand('vscode.executeLinkProvider', model.uri); + assert.strictEqual(links1.length, 1); + assert.strictEqual(links1[0].tooltip, 'Link Tooltip'); + }); + test('Color provider', function () { diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index dd77886bbf0..de02ffff745 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; diff --git a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts index f17a6c5b0e9..be11d3a7353 100644 --- a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { MainContext, IWorkspaceEditDto, MainThreadBulkEditsShape, IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; @@ -13,6 +13,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { @@ -25,8 +26,8 @@ suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { const rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(MainContext.MainThreadBulkEdits, new class extends mock() { - override $tryApplyWorkspaceEdit(_workspaceResourceEdits: IWorkspaceEditDto): Promise { - workspaceResourceEdits = _workspaceResourceEdits; + override $tryApplyWorkspaceEdit(_workspaceResourceEdits: SerializableObjectWithBuffers): Promise { + workspaceResourceEdits = _workspaceResourceEdits.value; return Promise.resolve(true); } }); diff --git a/src/vs/workbench/api/test/browser/extHostCommands.test.ts b/src/vs/workbench/api/test/browser/extHostCommands.test.ts index 5697ffe78cf..5353ca9c069 100644 --- a/src/vs/workbench/api/test/browser/extHostCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostCommands.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainThreadCommandsShape } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index 6e8a62e0ce9..298299b3e56 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; @@ -44,11 +44,11 @@ suite('ExtHostConfiguration', function () { function createConfigurationData(contents: any): IConfigurationInitData { return { - defaults: new ConfigurationModel(contents), - policy: new ConfigurationModel(), - application: new ConfigurationModel(), - user: new ConfigurationModel(contents), - workspace: new ConfigurationModel(), + defaults: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), + policy: ConfigurationModel.createEmptyModel(new NullLogService()), + application: ConfigurationModel.createEmptyModel(new NullLogService()), + user: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), + workspace: ConfigurationModel.createEmptyModel(new NullLogService()), folders: [], configurationScopes: [] }; @@ -284,15 +284,15 @@ suite('ExtHostConfiguration', function () { 'editor': { 'wordWrap': 'off' } - }, ['editor.wordWrap']), - policy: new ConfigurationModel(), - application: new ConfigurationModel(), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), + policy: ConfigurationModel.createEmptyModel(new NullLogService()), + application: ConfigurationModel.createEmptyModel(new NullLogService()), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } - }, ['editor.wordWrap']), - workspace: new ConfigurationModel({}, []), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), + workspace: new ConfigurationModel({}, [], [], undefined, new NullLogService()), folders: [], configurationScopes: [] }, @@ -319,7 +319,7 @@ suite('ExtHostConfiguration', function () { 'editor': { 'wordWrap': 'bounded' } - }, ['editor.wordWrap']); + }, ['editor.wordWrap'], [], undefined, new NullLogService()); folders.push([workspaceUri, workspace]); const extHostWorkspace = createExtHostWorkspace(); extHostWorkspace.$initializeWorkspace({ @@ -335,14 +335,14 @@ suite('ExtHostConfiguration', function () { 'editor': { 'wordWrap': 'off' } - }, ['editor.wordWrap']), - policy: new ConfigurationModel(), - application: new ConfigurationModel(), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), + policy: ConfigurationModel.createEmptyModel(new NullLogService()), + application: ConfigurationModel.createEmptyModel(new NullLogService()), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } - }, ['editor.wordWrap']), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), workspace, folders, configurationScopes: [] @@ -380,7 +380,7 @@ suite('ExtHostConfiguration', function () { 'editor': { 'wordWrap': 'bounded' } - }, ['editor.wordWrap']); + }, ['editor.wordWrap'], [], undefined, new NullLogService()); const firstRoot = URI.file('foo1'); const secondRoot = URI.file('foo2'); @@ -391,13 +391,13 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'off', 'lineNumbers': 'relative' } - }, ['editor.wordWrap'])]); + }, ['editor.wordWrap'], [], undefined, new NullLogService())]); folders.push([secondRoot, new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } - }, ['editor.wordWrap'])]); - folders.push([thirdRoot, new ConfigurationModel({}, [])]); + }, ['editor.wordWrap'], [], undefined, new NullLogService())]); + folders.push([thirdRoot, new ConfigurationModel({}, [], [], undefined, new NullLogService())]); const extHostWorkspace = createExtHostWorkspace(); extHostWorkspace.$initializeWorkspace({ @@ -414,14 +414,14 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'off', 'lineNumbers': 'on' } - }, ['editor.wordWrap']), - policy: new ConfigurationModel(), - application: new ConfigurationModel(), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), + policy: ConfigurationModel.createEmptyModel(new NullLogService()), + application: ConfigurationModel.createEmptyModel(new NullLogService()), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } - }, ['editor.wordWrap']), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), workspace, folders, configurationScopes: [] @@ -520,8 +520,8 @@ suite('ExtHostConfiguration', function () { 'editor.wordWrap': 'bounded', } }), - policy: new ConfigurationModel(), - application: new ConfigurationModel(), + policy: ConfigurationModel.createEmptyModel(new NullLogService()), + application: ConfigurationModel.createEmptyModel(new NullLogService()), user: toConfigurationModel({ 'editor.wordWrap': 'bounded', '[typescript]': { @@ -575,20 +575,20 @@ suite('ExtHostConfiguration', function () { 'lineNumbers': 'on', 'fontSize': '12px' } - }, ['editor.wordWrap']), - policy: new ConfigurationModel(), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), + policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } - }, ['editor.wordWrap']), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'auto', 'lineNumbers': 'off' } - }, ['editor.wordWrap']), - workspace: new ConfigurationModel({}, []), + }, ['editor.wordWrap'], [], undefined, new NullLogService()), + workspace: new ConfigurationModel({}, [], [], undefined, new NullLogService()), folders: [], configurationScopes: [] }, @@ -789,7 +789,7 @@ suite('ExtHostConfiguration', function () { } function toConfigurationModel(obj: any): ConfigurationModel { - const parser = new ConfigurationModelParser('test'); + const parser = new ConfigurationModelParser('test', new NullLogService()); parser.parse(JSON.stringify(obj)); return parser.configurationModel; } diff --git a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts index 26b419f6e06..8dca84bc5c1 100644 --- a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index 6c6de8c7e17..2c866daee54 100644 --- a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DiagnosticCollection, ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts index 2132482c3aa..f4795adebb4 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index 3f60f5ef898..298a2811954 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { Position } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index 59c02e4d3b1..9f0a28ec02a 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; @@ -16,6 +16,7 @@ import { mock } from 'vs/base/test/common/mock'; import { NullLogService } from 'vs/platform/log/common/log'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; function timeout(n: number) { return new Promise(resolve => setTimeout(resolve, n)); @@ -257,8 +258,8 @@ suite('ExtHostDocumentSaveParticipant', () => { let dto: IWorkspaceEditDto; const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { - $tryApplyWorkspaceEdit(_edits: IWorkspaceEditDto) { - dto = _edits; + $tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers) { + dto = _edits.value; return Promise.resolve(true); } }); @@ -281,8 +282,8 @@ suite('ExtHostDocumentSaveParticipant', () => { let edits: IWorkspaceEditDto; const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { - $tryApplyWorkspaceEdit(_edits: IWorkspaceEditDto) { - edits = _edits; + $tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers) { + edits = _edits.value; return Promise.resolve(true); } }); @@ -318,9 +319,9 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, two listeners -> two document states', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { - $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto) { + $tryApplyWorkspaceEdit(dto: SerializableObjectWithBuffers) { - for (const edit of dto.edits) { + for (const edit of dto.value.edits) { const uri = URI.revive((edit).resource); const { text, range } = (edit).textEdit; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts index 3f4255c49c8..eda97538749 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index cedfaa5e426..75f2ccce2d7 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; diff --git a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts index ba5e260f4d7..953aff7ea0c 100644 --- a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index 3909249dc09..ec934301cd9 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; @@ -23,7 +23,7 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import * as languages from 'vs/editor/common/languages'; import { getCodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition, getDeclarationsAtPosition, getReferencesAtPosition } from 'vs/editor/contrib/gotoSymbol/browser/goToSymbol'; -import { getHoverPromise } from 'vs/editor/contrib/hover/browser/getHover'; +import { getHoversPromise } from 'vs/editor/contrib/hover/browser/getHover'; import { getOccurrencesAtPosition } from 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; import { getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { getWorkspaceSymbols } from 'vs/workbench/contrib/search/common/search'; @@ -301,7 +301,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -322,7 +322,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 2); }); @@ -341,7 +341,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 2); // let [first, second] = value; assert.strictEqual(value[0].uri.authority, 'second'); @@ -362,7 +362,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); }); @@ -377,7 +377,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -395,7 +395,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -413,7 +413,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, new EditorPosition(1, 1), false, CancellationToken.None); assert.strictEqual(value.length, 1); const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -431,7 +431,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const hovers = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const hovers = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(hovers.length, 1); const [entry] = hovers; assert.deepStrictEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); @@ -447,7 +447,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const hovers = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const hovers = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(hovers.length, 1); const [entry] = hovers; assert.deepStrictEqual(entry.range, { startLineNumber: 4, startColumn: 1, endLineNumber: 9, endColumn: 8 }); @@ -469,9 +469,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(value.length, 2); - const [first, second] = (value as languages.Hover[]); + const [first, second] = value; assert.strictEqual(first.contents[0].value, 'registered second'); assert.strictEqual(second.contents[0].value, 'registered first'); }); @@ -491,7 +491,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const hovers = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const hovers = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(hovers.length, 1); }); @@ -517,7 +517,7 @@ suite('ExtHostLanguageFeatures', function () { disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentHighlightProvider { provideDocumentHighlights(): any { - return []; + return undefined; } })); disposables.add(extHost.registerDocumentHighlightProvider(defaultExtension, '*', new class implements vscode.DocumentHighlightProvider { @@ -591,7 +591,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, false, CancellationToken.None); assert.strictEqual(value.length, 2); const [first, second] = value; assert.strictEqual(first.uri.path, '/second'); @@ -607,7 +607,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, false, CancellationToken.None); assert.strictEqual(value.length, 1); const [item] = value; assert.deepStrictEqual(item.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); @@ -628,7 +628,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, false, CancellationToken.None); assert.strictEqual(value.length, 1); }); diff --git a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts index 465663c8288..a88e5a79bb9 100644 --- a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadMessageService } from 'vs/workbench/api/browser/mainThreadMessageService'; import { IDialogService, IPrompt, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 806a6b685dd..49a2f011645 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as vscode from 'vscode'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; @@ -66,7 +66,7 @@ suite('NotebookCell#Document', function () { override onExtensionError(): boolean { return true; } - }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); + }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, new NullLogService()); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); const reg = extHostNotebooks.registerNotebookSerializer(nullExtensionDescription, 'test', new class extends mock() { }); diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 8af3ece68d9..a1341bad58c 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -105,7 +105,7 @@ suite('NotebookKernel', function () { }); extHostConsumerFileSystem = new ExtHostConsumerFileSystem(rpcProtocol, new ExtHostFileSystemInfo()); extHostSearch = new ExtHostSearch(rpcProtocol, new URITransformerService(null), new NullLogService()); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, new NullLogService()); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); @@ -344,4 +344,3 @@ suite('NotebookKernel', function () { assert.ok(found); }); }); - diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 0f79b399d9d..54878ddb0cd 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -44,7 +44,8 @@ suite('ExtHostTelemetry', function () { firstSessionDate: '2020-01-01T00:00:00.000Z', sessionId: 'test', machineId: 'test', - sqmId: 'test' + sqmId: 'test', + devDeviceId: 'test' }; const mockRemote = { @@ -63,7 +64,8 @@ suite('ExtHostTelemetry', function () { publisher: 'vscode', version: '1.0.0', engines: { vscode: '*' }, - extensionLocation: URI.parse('fake') + extensionLocation: URI.parse('fake'), + enabledApiProposals: undefined, }; const createExtHostTelemetry = () => { diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index b79db583bb0..4a49c8031a2 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; +import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; @@ -13,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { mock, mockObject, MockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import * as editorRange from 'vs/editor/common/core/range'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -22,7 +23,7 @@ import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostTesting, TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting'; import { ExtHostTestItemCollection, TestItemImpl } from 'vs/workbench/api/common/extHostTestItem'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Location, Position, Range, TestMessage, TestResultState, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; +import { Location, Position, Range, TestMessage, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; import { AnyCallRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -85,11 +86,14 @@ suite('ExtHost Testing', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); let single: TestExtHostTestItemCollection; + let resolveCalls: (string | undefined)[] = []; setup(() => { + resolveCalls = []; single = ds.add(new TestExtHostTestItemCollection('ctrlId', 'root', { getDocument: () => undefined, } as Partial as ExtHostDocumentsAndEditors)); single.resolveHandler = item => { + resolveCalls.push(item?.id); if (item === undefined) { const a = new TestItemImpl('ctrlId', 'id-a', 'a', URI.file('/')); a.canResolveChildren = true; @@ -305,7 +309,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded, item: { label: 'Hello world' } }, + item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'Hello world' } }, }, { op: TestDiffOpType.DocumentSynced, @@ -326,6 +330,40 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), []); }); + suite('expandibility restoration', () => { + const doReplace = async (canResolveChildren = true) => { + const uri = single.root.children.get('id-a')!.uri; + const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', uri); + newA.canResolveChildren = canResolveChildren; + single.root.children.replace([ + newA, + new TestItemImpl('ctrlId', 'id-b', single.root.children.get('id-b')!.label, uri), + ]); + await timeout(0); // drain microtasks + }; + + test('does not restore an unexpanded state', async () => { + await single.expand(single.root.id, 0); + assert.deepStrictEqual(resolveCalls, [undefined]); + await doReplace(); + assert.deepStrictEqual(resolveCalls, [undefined]); + }); + + test('restores resolve state on replacement', async () => { + await single.expand(single.root.id, Infinity); + assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']); + await doReplace(); + assert.deepStrictEqual(resolveCalls, [undefined, 'id-a', 'id-a']); + }); + + test('does not expand if new child is not expandable', async () => { + await single.expand(single.root.id, Infinity); + assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']); + await doReplace(false); + assert.deepStrictEqual(resolveCalls, [undefined, 'id-a']); + }); + }); + test('treats in-place replacement as mutation deeply', () => { single.expand(single.root.id, Infinity); single.collectDiff(); @@ -340,10 +378,6 @@ suite('ExtHost Testing', () => { single.root.children.replace([newA, single.root.children.get('id-b')!]); assert.deepStrictEqual(single.collectDiff(), [ - { - op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded }, - }, { op: TestDiffOpType.Update, item: { extId: TestId.fromExtHostTestItem(oldAB, 'ctrlId').toString(), item: { label: 'Hello world' } }, @@ -603,6 +637,7 @@ suite('ExtHost Testing', () => { let req: TestRunRequest; let dto: TestRunDto; + const ext: IExtensionDescription = {} as any; teardown(() => { for (const { id } of c.trackers) { @@ -624,6 +659,7 @@ suite('ExtHost Testing', () => { include: undefined, exclude: [single.root.children.get('id-b')!], profile: configuration, + preserveFocus: false, }; dto = TestRunDto.fromInternal({ @@ -636,11 +672,11 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from a main thread request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); assert.strictEqual(tracker.hasRunningTasks, false); - const task1 = c.createTestRun('ctrl', single, req, 'run1', true); - const task2 = c.createTestRun('ctrl', single, req, 'run2', true); + const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); + const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); assert.strictEqual(proxy.$startedExtensionTestRun.called, false); assert.strictEqual(tracker.hasRunningTasks, true); @@ -661,8 +697,8 @@ suite('ExtHost Testing', () => { test('run cancel force ends after a timeout', () => { const clock = sinon.useFakeTimers(); try { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); - const task = c.createTestRun('ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); + const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -686,8 +722,8 @@ suite('ExtHost Testing', () => { }); test('run cancel force ends on second cancellation request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); - const task = c.createTestRun('ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); + const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -705,7 +741,7 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from an extension request', () => { - const task1 = c.createTestRun('ctrl', single, req, 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; assert.strictEqual(tracker.hasRunningTasks, true); @@ -718,11 +754,12 @@ suite('ExtHost Testing', () => { exclude: [new TestId(['ctrlId', 'id-b']).toString()], persist: false, continuous: false, + preserveFocus: false, }] ]); - const task2 = c.createTestRun('ctrl', single, req, 'run2', true); - const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true); + const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); + const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true); task1.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); @@ -736,7 +773,7 @@ suite('ExtHost Testing', () => { }); test('adds tests to run smartly', () => { - const task1 = c.createTestRun('ctrlId', single, req, 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; const expectedArgs: unknown[][] = []; assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); @@ -775,7 +812,7 @@ suite('ExtHost Testing', () => { const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt')); test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0)); single.root.children.replace([test1, test2]); - const task = c.createTestRun('ctrlId', single, req, 'hello world', false); + const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); const message1 = new TestMessage('some message'); message1.location = new Location(URI.file('/a.txt'), new Position(0, 0)); @@ -792,7 +829,8 @@ suite('ExtHost Testing', () => { expected: undefined, contextValue: undefined, actual: undefined, - location: convert.location.from(message1.location) + location: convert.location.from(message1.location), + stackTrace: undefined, }] ]); @@ -809,6 +847,7 @@ suite('ExtHost Testing', () => { expected: undefined, actual: undefined, location: convert.location.from({ uri: test2.uri!, range: test2.range }), + stackTrace: undefined, }] ]); @@ -816,7 +855,7 @@ suite('ExtHost Testing', () => { }); test('guards calls after runs are ended', () => { - const task = c.createTestRun('ctrl', single, req, 'hello world', false); + const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); task.end(); task.failed(single.root, new TestMessage('some message')); @@ -827,28 +866,6 @@ suite('ExtHost Testing', () => { assert.strictEqual(proxy.$appendTestMessagesInRun.called, false); }); - test('excludes tests outside tree or explicitly excluded', () => { - const task = c.createTestRun('ctrlId', single, { - profile: configuration, - include: [single.root.children.get('id-a')!], - exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], - }, 'hello world', false); - - task.passed(single.root.children.get('id-a')!.children.get('id-aa')!); - task.passed(single.root.children.get('id-a')!.children.get('id-ab')!); - - assert.deepStrictEqual(proxy.$updateTestStateInRun.args.length, 1); - const args = proxy.$updateTestStateInRun.args[0]; - assert.deepStrictEqual(proxy.$updateTestStateInRun.args, [[ - args[0], - args[1], - new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), - TestResultState.Passed, - undefined, - ]]); - task.end(); - }); - test('sets state of test with identical local IDs (#131827)', () => { const testA = single.root.children.get('id-a'); const testB = single.root.children.get('id-b'); @@ -857,7 +874,7 @@ suite('ExtHost Testing', () => { const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined); testB!.children.replace([childB]); - const task1 = c.createTestRun('ctrl', single, new TestRunRequestImpl(), 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false); const tracker = Iterable.first(c.trackers)!; task1.passed(childA); diff --git a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts index 106a30df4f8..98bc7766151 100644 --- a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts @@ -2,7 +2,7 @@ * 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 assert from 'assert'; import { Lazy } from 'vs/base/common/lazy'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 3f780a03299..1e442c1ed88 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { Emitter } from 'vs/base/common/event'; import { ExtHostTreeViews } from 'vs/workbench/api/common/extHostTreeViews'; diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index ab38b202b6e..40a148042ff 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { MarkdownString, NotebookCellOutputItem, NotebookData, LanguageSelector, WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index 6cef861c9c8..3d1ff69b232 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/api/test/browser/extHostWebview.test.ts b/src/vs/workbench/api/test/browser/extHostWebview.test.ts index b741292bd00..e87300aeb05 100644 --- a/src/vs/workbench/api/test/browser/extHostWebview.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWebview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts index 6abca46d3ff..5b9dd312c74 100644 --- a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { basename } from 'vs/base/common/path'; import { URI, UriComponents } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts b/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts index 959705f233c..c6ef6939bfe 100644 --- a/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts b/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts index 0c5d47544d1..d7503ca0d50 100644 --- a/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts index a3a8e47755c..0c5a02f6606 100644 --- a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts index bf67789ff10..2f89e98e358 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts index 8eaa58798d7..949c2a78ffa 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { MainThreadDocumentContentProviders } from 'vs/workbench/api/browser/mainThreadDocumentContentProviders'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts index 8331e51cc03..fee65f21119 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; import { timeout } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index d7190f1fe5c..17a9049d064 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -27,11 +27,15 @@ import { TestTextResourcePropertiesService, TestWorkingCopyFileService } from 'v import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { LanguageService } from 'vs/editor/common/services/languageService'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; suite('MainThreadDocumentsAndEditors', () => { @@ -61,12 +65,15 @@ suite('MainThreadDocumentsAndEditors', () => { const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); + const instantiationService = new TestInstantiationService(); + instantiationService.set(ILanguageService, disposables.add(new LanguageService())); + instantiationService.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); + instantiationService.set(IUndoRedoService, undoRedoService); modelService = new ModelService( configService, new TestTextResourcePropertiesService(configService), undoRedoService, - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), + instantiationService ); codeEditorService = new TestCodeEditorService(themeService); textFileService = new class extends mock() { diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 78d484a667d..d83a5ebcf42 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore, IReference, ImmortalReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -16,12 +16,10 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { LanguageService } from 'vs/editor/common/services/languageService'; import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -48,6 +46,7 @@ import { BulkEditService } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditS import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; @@ -56,6 +55,10 @@ import { ICopyOperation, ICreateFileOperation, ICreateOperation, IDeleteOperatio import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestFileService, TestLifecycleService, TestWorkingCopyService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('MainThreadEditors', () => { @@ -79,19 +82,11 @@ suite('MainThreadEditors', () => { createdResources.clear(); deletedResources.clear(); - const configService = new TestConfigurationService(); const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); - modelService = new ModelService( - configService, - new TestTextResourcePropertiesService(configService), - undoRedoService, - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), - ); const services = new ServiceCollection(); services.set(IBulkEditService, new SyncDescriptor(BulkEditService)); @@ -177,8 +172,18 @@ suite('MainThreadEditors', () => { } }); + services.set(ILanguageService, disposables.add(new LanguageService())); + services.set(ILanguageConfigurationService, new TestLanguageConfigurationService()); + const instaService = new InstantiationService(services); + modelService = new ModelService( + configService, + new TestTextResourcePropertiesService(configService), + undoRedoService, + instaService + ); + bulkEdits = instaService.createInstance(MainThreadBulkEdits, SingleProxyRPCProtocol(null)); }); @@ -204,7 +209,7 @@ suite('MainThreadEditors', () => { // Act as if the user edited the model model.applyEdits([EditOperation.insert(new Position(0, 0), 'something')]); - return bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit] }).then((result) => { + return bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit] })).then((result) => { assert.strictEqual(result, false); }); }); @@ -230,11 +235,11 @@ suite('MainThreadEditors', () => { } }; - const p1 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit1] }).then((result) => { + const p1 = bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit1] })).then((result) => { // first edit request succeeds assert.strictEqual(result, true); }); - const p2 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit2] }).then((result) => { + const p2 = bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit2] })).then((result) => { // second edit request fails assert.strictEqual(result, false); }); @@ -242,13 +247,13 @@ suite('MainThreadEditors', () => { }); test(`applyWorkspaceEdit with only resource edit`, () => { - return bulkEdits.$tryApplyWorkspaceEdit({ + return bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [ { oldResource: resource, newResource: resource, options: undefined }, { oldResource: undefined, newResource: resource, options: undefined }, { oldResource: resource, newResource: undefined, options: undefined } ] - }).then((result) => { + })).then((result) => { assert.strictEqual(result, true); assert.strictEqual(movedResources.get(resource), resource); assert.strictEqual(createdResources.has(resource), true); diff --git a/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts index 8a714d8130d..dd20e3cd70c 100644 --- a/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { disposableTimeout, timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index ada06633cf8..f4ead1836b2 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -54,7 +54,7 @@ suite('MainThreadHostTreeView', function () { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(async () => { - const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); + const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); const viewDescriptorService = disposables.add(instantiationService.createInstance(ViewDescriptorService)); instantiationService.stub(IViewDescriptorService, viewDescriptorService); container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', title: nls.localize2('test', 'test'), ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts b/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts index 5234d7abb34..c784f418950 100644 --- a/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts index 6f37db49441..bfd874acf2e 100644 --- a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers, timeout } from 'vs/base/common/async'; +import { Mutable } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionsActivator, IExtensionsActivatorHost } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtensionDescriptionRegistry, IActivationEventsReader } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; @@ -14,6 +16,8 @@ import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbe suite('ExtensionsActivator', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + const idA = new ExtensionIdentifier(`a`); const idB = new ExtensionIdentifier(`b`); const idC = new ExtensionIdentifier(`c`); @@ -82,8 +86,8 @@ suite('ExtensionsActivator', () => { test('Supports having resolved extensions', async () => { const host = new SimpleExtensionsActivatorHost(); const bExt = desc(idB); - delete (bExt).main; - delete (bExt).browser; + delete (>bExt).main; + delete (>bExt).browser; const activator = createActivator(host, [ desc(idA, [idB]) ], [bExt]); @@ -100,7 +104,7 @@ suite('ExtensionsActivator', () => { [idB, extActivationB] ]); const bExt = desc(idB); - (bExt).api = 'none'; + (>bExt).api = 'none'; const activator = createActivator(host, [ desc(idA, [idB]) ], [bExt]); @@ -271,7 +275,8 @@ suite('ExtensionsActivator', () => { activationEvents, main: 'index.js', targetPlatform: TargetPlatform.UNDEFINED, - extensionDependencies: deps.map(d => d.value) + extensionDependencies: deps.map(d => d.value), + enabledApiProposals: undefined, }; } diff --git a/src/vs/workbench/api/test/common/extensionHostMain.test.ts b/src/vs/workbench/api/test/common/extensionHostMain.test.ts index 928c06a1b2c..1608511b527 100644 --- a/src/vs/workbench/api/test/common/extensionHostMain.test.ts +++ b/src/vs/workbench/api/test/common/extensionHostMain.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SerializedError, errorHandler, onUnexpectedError } from 'vs/base/common/errors'; import { isFirefox, isSafari } from 'vs/base/common/platform'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -48,7 +48,7 @@ suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slo declare readonly _serviceBrand: undefined; getExtensionPathIndex() { return new class extends ExtensionPaths { - override findSubstr(key: URI): Readonly | undefined { + override findSubstr(key: URI): IExtensionDescription | undefined { findSubstrCount++; return nullExtensionDescription; } diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 1502ef3f565..58442641724 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mapArrayOrNot } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -16,6 +16,7 @@ import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { Range } from 'vs/workbench/api/common/extHostTypes'; import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; @@ -144,6 +145,26 @@ suite('ExtHostSearch', () => { rpcProtocol, new class extends mock() { override remote = { isRemote: false, authority: undefined, connectionData: null }; }, new URITransformerService(null), + new class extends mock() { + override async getConfigProvider(): Promise { + return { + onDidChangeConfiguration(_listener: (event: vscode.ConfigurationChangeEvent) => void) { }, + getConfiguration(): vscode.WorkspaceConfiguration { + return { + get() { }, + has() { + return false; + }, + inspect() { + return undefined; + }, + async update() { } + }; + }, + + } as ExtHostConfigProvider; + } + }, logService ); this._pfs = mockPFS as any; diff --git a/src/vs/workbench/api/test/node/extHostTunnelService.test.ts b/src/vs/workbench/api/test/node/extHostTunnelService.test.ts index 2f567ef7f4b..57fe15e52b4 100644 --- a/src/vs/workbench/api/test/node/extHostTunnelService.test.ts +++ b/src/vs/workbench/api/test/node/extHostTunnelService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { findPorts, getRootProcesses, getSockets, loadConnectionTable, loadListeningPorts, parseIpAddress, tryFindRootPorts } from 'vs/workbench/api/node/extHostTunnelService'; const tcp = diff --git a/src/vs/workbench/browser/actions.ts b/src/vs/workbench/browser/actions.ts index 71fea8cc6d3..dbe7355c8c2 100644 --- a/src/vs/workbench/browser/actions.ts +++ b/src/vs/workbench/browser/actions.ts @@ -23,7 +23,7 @@ class MenuActions extends Disposable { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; - private disposables = this._register(new DisposableStore()); + private readonly disposables = this._register(new DisposableStore()); constructor( menuId: MenuId, @@ -96,9 +96,8 @@ export class CompositeMenuActions extends Disposable { const actions: IAction[] = []; if (this.contextMenuId) { - const menu = this.menuService.createMenu(this.contextMenuId, this.contextKeyService); - createAndFillInActionBarActions(menu, this.options, { primary: [], secondary: actions }); - menu.dispose(); + const menu = this.menuService.getMenuActions(this.contextMenuId, this.contextKeyService, this.options); + createAndFillInActionBarActions(menu, { primary: [], secondary: actions }); } return actions; diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 2c20a3572e2..7823fa988c0 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -63,7 +63,7 @@ class InspectContextKeysAction extends Action2 { const hoverFeedback = document.createElement('div'); const activeDocument = getActiveDocument(); activeDocument.body.appendChild(hoverFeedback); - disposables.add(toDisposable(() => activeDocument.body.removeChild(hoverFeedback))); + disposables.add(toDisposable(() => hoverFeedback.remove())); hoverFeedback.style.position = 'absolute'; hoverFeedback.style.pointerEvents = 'none'; diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 765693c16c8..bba90710822 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from 'vs/nls'; +import { ILocalizedString, localize, localize2 } from 'vs/nls'; import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,6 +31,7 @@ import { ICommandActionTitle } from 'vs/platform/action/common/action'; import { mainWindow } from 'vs/base/browser/window'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { TitlebarStyle } from 'vs/platform/window/common/window'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; // Register Icons const menubarIcon = registerIcon('menuBar', Codicon.layoutMenubar, localize('menuBarIcon', "Represents the menu bar")); @@ -447,12 +448,13 @@ registerAction2(ToggleStatusbarVisibilityAction); abstract class AbstractSetShowTabsAction extends Action2 { - constructor(private readonly settingName: string, private readonly value: string, title: ICommandActionTitle, id: string, precondition: ContextKeyExpression) { + constructor(private readonly settingName: string, private readonly value: string, title: ICommandActionTitle, id: string, precondition: ContextKeyExpression, description: string | ILocalizedString | undefined) { super({ id, title, category: Categories.View, precondition, + metadata: description ? { description } : undefined, f1: true }); } @@ -472,7 +474,7 @@ export class HideEditorTabsAction extends AbstractSetShowTabsAction { constructor() { const precondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_TABS_MODE}`, EditorTabsMode.NONE).negate(), InEditorZenModeContext.negate())!; const title = localize2('hideEditorTabs', 'Hide Editor Tabs'); - super(LayoutSettings.EDITOR_TABS_MODE, EditorTabsMode.NONE, title, HideEditorTabsAction.ID, precondition); + super(LayoutSettings.EDITOR_TABS_MODE, EditorTabsMode.NONE, title, HideEditorTabsAction.ID, precondition, localize2('hideEditorTabsDescription', "Hide Tab Bar")); } } @@ -483,7 +485,7 @@ export class ZenHideEditorTabsAction extends AbstractSetShowTabsAction { constructor() { const precondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ZenModeSettings.SHOW_TABS}`, EditorTabsMode.NONE).negate(), InEditorZenModeContext)!; const title = localize2('hideEditorTabsZenMode', 'Hide Editor Tabs in Zen Mode'); - super(ZenModeSettings.SHOW_TABS, EditorTabsMode.NONE, title, ZenHideEditorTabsAction.ID, precondition); + super(ZenModeSettings.SHOW_TABS, EditorTabsMode.NONE, title, ZenHideEditorTabsAction.ID, precondition, localize2('hideEditorTabsZenModeDescription', "Hide Tab Bar in Zen Mode")); } } @@ -497,7 +499,7 @@ export class ShowMultipleEditorTabsAction extends AbstractSetShowTabsAction { const precondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_TABS_MODE}`, EditorTabsMode.MULTIPLE).negate(), InEditorZenModeContext.negate())!; const title = localize2('showMultipleEditorTabs', 'Show Multiple Editor Tabs'); - super(LayoutSettings.EDITOR_TABS_MODE, EditorTabsMode.MULTIPLE, title, ShowMultipleEditorTabsAction.ID, precondition); + super(LayoutSettings.EDITOR_TABS_MODE, EditorTabsMode.MULTIPLE, title, ShowMultipleEditorTabsAction.ID, precondition, localize2('showMultipleEditorTabsDescription', "Show Tab Bar with multiple tabs")); } } @@ -509,7 +511,7 @@ export class ZenShowMultipleEditorTabsAction extends AbstractSetShowTabsAction { const precondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ZenModeSettings.SHOW_TABS}`, EditorTabsMode.MULTIPLE).negate(), InEditorZenModeContext)!; const title = localize2('showMultipleEditorTabsZenMode', 'Show Multiple Editor Tabs in Zen Mode'); - super(ZenModeSettings.SHOW_TABS, EditorTabsMode.MULTIPLE, title, ZenShowMultipleEditorTabsAction.ID, precondition); + super(ZenModeSettings.SHOW_TABS, EditorTabsMode.MULTIPLE, title, ZenShowMultipleEditorTabsAction.ID, precondition, localize2('showMultipleEditorTabsZenModeDescription', "Show Tab Bar in Zen Mode")); } } @@ -523,7 +525,7 @@ export class ShowSingleEditorTabAction extends AbstractSetShowTabsAction { const precondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_TABS_MODE}`, EditorTabsMode.SINGLE).negate(), InEditorZenModeContext.negate())!; const title = localize2('showSingleEditorTab', 'Show Single Editor Tab'); - super(LayoutSettings.EDITOR_TABS_MODE, EditorTabsMode.SINGLE, title, ShowSingleEditorTabAction.ID, precondition); + super(LayoutSettings.EDITOR_TABS_MODE, EditorTabsMode.SINGLE, title, ShowSingleEditorTabAction.ID, precondition, localize2('showSingleEditorTabDescription', "Show Tab Bar with one Tab")); } } @@ -535,7 +537,7 @@ export class ZenShowSingleEditorTabAction extends AbstractSetShowTabsAction { const precondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ZenModeSettings.SHOW_TABS}`, EditorTabsMode.SINGLE).negate(), InEditorZenModeContext)!; const title = localize2('showSingleEditorTabZenMode', 'Show Single Editor Tab in Zen Mode'); - super(ZenModeSettings.SHOW_TABS, EditorTabsMode.SINGLE, title, ZenShowSingleEditorTabAction.ID, precondition); + super(ZenModeSettings.SHOW_TABS, EditorTabsMode.SINGLE, title, ZenShowSingleEditorTabAction.ID, precondition, localize2('showSingleEditorTabZenModeDescription', "Show Tab Bar in Zen Mode with one Tab")); } } @@ -576,6 +578,7 @@ export class EditorActionsTitleBarAction extends Action2 { title: localize2('moveEditorActionsToTitleBar', "Move Editor Actions to Title Bar"), category: Categories.View, precondition: ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_ACTIONS_LOCATION}`, EditorActionsLocation.TITLEBAR).negate(), + metadata: { description: localize2('moveEditorActionsToTitleBarDescription', "Move Editor Actions from the tab bar to the title bar") }, f1: true }); } @@ -602,6 +605,7 @@ export class EditorActionsDefaultAction extends Action2 { ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_ACTIONS_LOCATION}`, EditorActionsLocation.DEFAULT).negate(), ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_TABS_MODE}`, EditorTabsMode.NONE).negate(), ), + metadata: { description: localize2('moveEditorActionsToTabBarDescription', "Move Editor Actions from the title bar to the tab bar") }, f1: true }); } @@ -625,6 +629,7 @@ export class HideEditorActionsAction extends Action2 { title: localize2('hideEditorActons', "Hide Editor Actions"), category: Categories.View, precondition: ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_ACTIONS_LOCATION}`, EditorActionsLocation.HIDDEN).negate(), + metadata: { description: localize2('hideEditorActonsDescription', "Hide Editor Actions in the tab and title bar") }, f1: true }); } @@ -648,6 +653,7 @@ export class ShowEditorActionsAction extends Action2 { title: localize2('showEditorActons', "Show Editor Actions"), category: Categories.View, precondition: ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_ACTIONS_LOCATION}`, EditorActionsLocation.HIDDEN), + metadata: { description: localize2('showEditorActonsDescription', "Make Editor Actions visible.") }, f1: true }); } @@ -668,6 +674,48 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 11 }); +// --- Configure Tabs Layout + +export class ConfigureEditorTabsAction extends Action2 { + + static readonly ID = 'workbench.action.configureEditorTabs'; + + constructor() { + super({ + id: ConfigureEditorTabsAction.ID, + title: localize2('configureTabs', "Configure Tabs"), + category: Categories.View, + }); + } + + run(accessor: ServicesAccessor) { + const preferencesService = accessor.get(IPreferencesService); + preferencesService.openSettings({ jsonEditor: false, query: 'workbench.editor tab' }); + } +} +registerAction2(ConfigureEditorTabsAction); + +// --- Configure Editor + +export class ConfigureEditorAction extends Action2 { + + static readonly ID = 'workbench.action.configureEditor'; + + constructor() { + super({ + id: ConfigureEditorAction.ID, + title: localize2('configureEditors', "Configure Editors"), + category: Categories.View, + }); + } + + run(accessor: ServicesAccessor) { + const preferencesService = accessor.get(IPreferencesService); + preferencesService.openSettings({ jsonEditor: false, query: 'workbench.editor' }); + } +} +registerAction2(ConfigureEditorAction); + // --- Toggle Pinned Tabs On Separate Row registerAction2(class extends Action2 { @@ -678,6 +726,7 @@ registerAction2(class extends Action2 { title: localize2('toggleSeparatePinnedEditorTabs', "Separate Pinned Editor Tabs"), category: Categories.View, precondition: ContextKeyExpr.equals(`config.${LayoutSettings.EDITOR_TABS_MODE}`, EditorTabsMode.MULTIPLE), + metadata: { description: localize2('toggleSeparatePinnedEditorTabsDescription', "Toggle whether pinned editor tabs are shown on a separate row above unpinned tabs.") }, f1: true }); } @@ -917,7 +966,7 @@ registerAction2(class extends Action2 { } private async getView(quickInputService: IQuickInputService, viewDescriptorService: IViewDescriptorService, paneCompositePartService: IPaneCompositePartService, viewId?: string): Promise { - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); quickPick.placeholder = localize('moveFocusedView.selectView', "Select a View to Move"); quickPick.items = this.getViewItems(viewDescriptorService, paneCompositePartService); quickPick.selectedItems = quickPick.items.filter(item => (item as IQuickPickItem).id === viewId) as IQuickPickItem[]; @@ -976,7 +1025,7 @@ class MoveFocusedViewAction extends Action2 { return; } - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); quickPick.placeholder = localize('moveFocusedView.selectDestination', "Select a Destination for the View"); quickPick.title = localize({ key: 'moveFocusedView.title', comment: ['{0} indicates the title of the view the user has selected to move.'] }, "View: Move {0}", viewDescriptor.name.value); @@ -1362,7 +1411,7 @@ for (const { active } of [...ToggleVisibilityActions, ...MoveSideBarActions, ... registerAction2(class CustomizeLayoutAction extends Action2 { - private _currentQuickPick?: IQuickPick; + private _currentQuickPick?: IQuickPick; constructor() { super({ @@ -1455,7 +1504,7 @@ registerAction2(class CustomizeLayoutAction extends Action2 { const commandService = accessor.get(ICommandService); const quickInputService = accessor.get(IQuickInputService); const keybindingService = accessor.get(IKeybindingService); - const quickPick = quickInputService.createQuickPick(); + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); this._currentQuickPick = quickPick; quickPick.items = this.getItems(contextKeyService, keybindingService); diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index dffef61a015..188a94e1f51 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { List } from 'vs/base/browser/ui/list/listWidget'; @@ -22,6 +22,7 @@ import { isActiveElement } from 'vs/base/browser/dom'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { localize, localize2 } from 'vs/nls'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; function ensureDOMFocus(widget: ListWidget | undefined): void { // it can happen that one of the commands is executed while @@ -694,6 +695,57 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.showHover', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI), + when: WorkbenchListFocusContextKey, + handler: async (accessor: ServicesAccessor, ...args: any[]) => { + const listService = accessor.get(IListService); + const lastFocusedList = listService.lastFocusedList; + if (!lastFocusedList) { + return; + } + + // Check if a tree element is focused + const focus = lastFocusedList.getFocus(); + if (!focus || (focus.length === 0)) { + return; + } + + // As the tree does not know anything about the rendered DOM elements + // we have to traverse the dom to find the HTMLElements + const treeDOM = lastFocusedList.getHTMLElement(); + const scrollableElement = treeDOM.querySelector('.monaco-scrollable-element'); + const listRows = scrollableElement?.querySelector('.monaco-list-rows'); + const focusedElement = listRows?.querySelector('.focused'); + if (!focusedElement) { + return; + } + + const elementWithHover = getCustomHoverForElement(focusedElement as HTMLElement); + if (elementWithHover) { + accessor.get(IHoverService).showManagedHover(elementWithHover as HTMLElement); + } + }, +}); + +function getCustomHoverForElement(element: HTMLElement): HTMLElement | undefined { + // Check if the element itself has a hover + if (element.matches('[custom-hover="true"]')) { + return element; + } + + // Only consider children that are not action items or have a tabindex + // as these element are focusable and the user is able to trigger them already + const noneFocusableElementWithHover = element.querySelector('[custom-hover="true"]:not([tabindex]):not(.action-item)'); + if (noneFocusableElementWithHover) { + return noneFocusableElementWithHover as HTMLElement; + } + + return undefined; +} + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.toggleExpand', weight: KeybindingWeight.WorkbenchContrib, @@ -805,7 +857,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.find', weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(RawWorkbenchListFocusContextKey, WorkbenchListSupportsFind), - primary: KeyMod.CtrlCmd | KeyCode.KeyF, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF, secondary: [KeyCode.F3], handler: (accessor) => { const widget = accessor.get(IListService).lastFocusedList; @@ -917,6 +969,7 @@ registerAction2(class ToggleStickyScroll extends Action2 { mnemonicTitle: localize({ key: 'mitoggleTreeStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Tree Sticky Scroll"), }, category: 'View', + metadata: { description: localize('toggleTreeStickyScrollDescription', "Toggles Sticky Scroll widget at the top of tree structures such as the File Explorer and Debug variables View.") }, f1: true }); } diff --git a/src/vs/workbench/browser/actions/media/actions.css b/src/vs/workbench/browser/actions/media/actions.css index 1e8b67d48f4..0d71343d1e0 100644 --- a/src/vs/workbench/browser/actions/media/actions.css +++ b/src/vs/workbench/browser/actions/media/actions.css @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ .monaco-workbench .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before { - content: "\ea76"; /* Close icon flips between black dot and "X" for dirty workspaces */ + /* Close icon flips between black dot and "X" for dirty workspaces */ + content: var(--vscode-icon-x-content); + font-family: var(--vscode-icon-x-font-family); } .monaco-workbench .screencast-mouse { diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index e0e838c85bf..56b88fe52d6 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -273,10 +273,13 @@ abstract class BaseFocusAction extends Action2 { neighbour = next ? Parts.PANEL_PART : Parts.SIDEBAR_PART; break; case Parts.PANEL_PART: - neighbour = next ? Parts.STATUSBAR_PART : Parts.EDITOR_PART; + neighbour = next ? Parts.AUXILIARYBAR_PART : Parts.EDITOR_PART; + break; + case Parts.AUXILIARYBAR_PART: + neighbour = next ? Parts.STATUSBAR_PART : Parts.PANEL_PART; break; case Parts.STATUSBAR_PART: - neighbour = next ? Parts.ACTIVITYBAR_PART : Parts.PANEL_PART; + neighbour = next ? Parts.ACTIVITYBAR_PART : Parts.AUXILIARYBAR_PART; break; case Parts.ACTIVITYBAR_PART: neighbour = next ? Parts.SIDEBAR_PART : Parts.STATUSBAR_PART; @@ -306,6 +309,8 @@ abstract class BaseFocusAction extends Action2 { currentlyFocusedPart = Parts.STATUSBAR_PART; } else if (layoutService.hasFocus(Parts.SIDEBAR_PART)) { currentlyFocusedPart = Parts.SIDEBAR_PART; + } else if (layoutService.hasFocus(Parts.AUXILIARYBAR_PART)) { + currentlyFocusedPart = Parts.AUXILIARYBAR_PART; } else if (layoutService.hasFocus(Parts.PANEL_PART)) { currentlyFocusedPart = Parts.PANEL_PART; } diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index 9890994cfdf..acf6652d837 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -163,12 +163,13 @@ registerAction2(class QuickAccessAction extends Action2 { run(accessor: ServicesAccessor): void { const quickInputService = accessor.get(IQuickInputService); + const providerOptions: AnythingQuickAccessProviderRunOptions = { + includeHelp: true, + from: 'commandCenter', + }; quickInputService.quickAccess.show(undefined, { preserveValue: true, - providerOptions: { - includeHelp: true, - from: 'commandCenter', - } as AnythingQuickAccessProviderRunOptions + providerOptions }); } }); diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index dae9d15bd89..c06fb52ca30 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Disposable } from 'vs/base/common/lifecycle'; -import { EventHelper, addDisposableListener, getActiveDocument, getWindow } from 'vs/base/browser/dom'; +import { EventHelper, addDisposableListener, getActiveDocument, getWindow, isHTMLElement, isHTMLInputElement, isHTMLTextAreaElement } from 'vs/base/browser/dom'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { isNative } from 'vs/base/common/platform'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -54,8 +54,8 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo else { const clipboardText = await this.clipboardService.readText(); if ( - element instanceof HTMLTextAreaElement || - element instanceof HTMLInputElement + isHTMLTextAreaElement(element) || + isHTMLInputElement(element) ) { const selectionStart = element.selectionStart || 0; const selectionEnd = element.selectionEnd || 0; @@ -88,7 +88,7 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo } const target = e.target; - if (!(target instanceof HTMLElement) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { + if (!(isHTMLElement(target)) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { return; // only for inputs or textareas } diff --git a/src/vs/workbench/browser/actions/widgetNavigationCommands.ts b/src/vs/workbench/browser/actions/widgetNavigationCommands.ts index bc2cb028dd2..1657bf72263 100644 --- a/src/vs/workbench/browser/actions/widgetNavigationCommands.ts +++ b/src/vs/workbench/browser/actions/widgetNavigationCommands.ts @@ -10,6 +10,8 @@ import { WorkbenchListFocusContextKey, WorkbenchListScrollAtBottomContextKey, Wo import { Event } from 'vs/base/common/event'; import { combinedDisposable, toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; /** INavigableContainer represents a logical container composed of widgets that can be navigated back and forth with key shortcuts */ @@ -25,6 +27,7 @@ interface INavigableContainer { * focused, and blurred if all parts being blurred. */ readonly focusNotifiers: readonly IFocusNotifier[]; + readonly name?: string; // for debugging focusPreviousWidget(): void; focusNextWidget(): void; } @@ -34,16 +37,18 @@ interface IFocusNotifier { readonly onDidBlur: Event; } -function handleFocusEventsGroup(group: readonly IFocusNotifier[], handler: (isFocus: boolean) => void): IDisposable { +function handleFocusEventsGroup(group: readonly IFocusNotifier[], handler: (isFocus: boolean) => void, onPartFocusChange?: (index: number, state: string) => void): IDisposable { const focusedIndices = new Set(); return combinedDisposable(...group.map((events, index) => combinedDisposable( events.onDidFocus(() => { + onPartFocusChange?.(index, 'focus'); if (!focusedIndices.size) { handler(true); } focusedIndices.add(index); }), events.onDidBlur(() => { + onPartFocusChange?.(index, 'blur'); focusedIndices.delete(index); if (!focusedIndices.size) { handler(false); @@ -65,7 +70,10 @@ class NavigableContainerManager implements IDisposable { private focused: IContextKey; - constructor(@IContextKeyService contextKeyService: IContextKeyService) { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ILogService private logService: ILogService, + @IConfigurationService private configurationService: IConfigurationService) { this.focused = NavigableContainerFocusedContextKey.bindTo(contextKeyService); NavigableContainerManager.INSTANCE = this; } @@ -76,25 +84,43 @@ class NavigableContainerManager implements IDisposable { NavigableContainerManager.INSTANCE = undefined; } + private get debugEnabled(): boolean { + return this.configurationService.getValue('workbench.navigibleContainer.enableDebug'); + } + + private log(msg: string, ...args: any[]): void { + if (this.debugEnabled) { + this.logService.debug(msg, ...args); + } + } + static register(container: INavigableContainer): IDisposable { const instance = this.INSTANCE; if (!instance) { return Disposable.None; } instance.containers.add(container); + instance.log('NavigableContainerManager.register', container.name); return combinedDisposable( handleFocusEventsGroup(container.focusNotifiers, (isFocus) => { if (isFocus) { + instance.log('NavigableContainerManager.focus', container.name); instance.focused.set(true); instance.lastContainer = container; - } else if (instance.lastContainer === container) { - instance.focused.set(false); - instance.lastContainer = undefined; + } else { + instance.log('NavigableContainerManager.blur', container.name, instance.lastContainer?.name); + if (instance.lastContainer === container) { + instance.focused.set(false); + instance.lastContainer = undefined; + } } + }, (index: number, event: string) => { + instance.log('NavigableContainerManager.partFocusChange', container.name, index, event); }), toDisposable(() => { instance.containers.delete(container); + instance.log('NavigableContainerManager.unregister', container.name, instance.lastContainer?.name); if (instance.lastContainer === container) { instance.focused.set(false); instance.lastContainer = undefined; diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 4020ac2b0e6..26c0b61d3d1 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -34,7 +34,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { isFolderBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/common/backup'; -import { getActiveElement, getActiveWindow } from 'vs/base/browser/dom'; +import { getActiveElement, getActiveWindow, isHTMLElement } from 'vs/base/browser/dom'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -404,7 +404,7 @@ class BlurAction extends Action2 { run(): void { const activeElement = getActiveElement(); - if (activeElement instanceof HTMLElement) { + if (isHTMLElement(activeElement)) { activeElement.blur(); } } diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 71ec7e27232..cffce2ce267 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,44 +7,30 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys'; -import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, MainEditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext, TitleBarStyleContext, MultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; -import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorGroupLockedContext, MultipleEditorGroupsContext, EditorsVisibleContext } from 'vs/workbench/common/contextkeys'; import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow, getActiveWindow } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { WorkbenchState, IWorkspaceContextService, isTemporaryWorkspace } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { getVirtualWorkspaceScheme } from 'vs/platform/workspace/common/virtualWorkspace'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; -import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; import { IProductService } from 'vs/platform/product/common/productService'; -import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { getTitleBarStyle } from 'vs/platform/window/common/window'; import { mainWindow } from 'vs/base/browser/window'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isFullscreen, onDidChangeFullscreen } from 'vs/base/browser/browser'; -import { Schemas } from 'vs/base/common/network'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; private dirtyWorkingCopiesContext: IContextKey; - private activeEditorContext: IContextKey; - private activeEditorCanRevert: IContextKey; - private activeEditorCanSplitInGroup: IContextKey; - private activeEditorAvailableEditorIds: IContextKey; - - private activeEditorIsReadonly: IContextKey; - private activeCompareEditorCanSwap: IContextKey; - private activeEditorCanToggleReadonly: IContextKey; - private activeEditorGroupEmpty: IContextKey; private activeEditorGroupIndex: IContextKey; private activeEditorGroupLast: IContextKey; @@ -53,10 +39,6 @@ export class WorkbenchContextKeysHandler extends Disposable { private editorsVisibleContext: IContextKey; - private textCompareEditorVisibleContext: IContextKey; - private textCompareEditorActiveContext: IContextKey; - - private sideBySideEditorActiveContext: IContextKey; private splitEditorsVerticallyContext: IContextKey; private workbenchStateContext: IContextKey; @@ -90,13 +72,11 @@ export class WorkbenchContextKeysHandler extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, - @IEditorService private readonly editorService: IEditorService, - @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @IEditorService private readonly editorService: IEditorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @IFileService private readonly fileService: IFileService ) { super(); @@ -128,24 +108,16 @@ export class WorkbenchContextKeysHandler extends Disposable { ProductQualityContext.bindTo(this.contextKeyService).set(this.productService.quality || ''); EmbedderIdentifierContext.bindTo(this.contextKeyService).set(productService.embedderIdentifier); - // Editors - this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); - this.activeEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.contextKeyService); - this.activeCompareEditorCanSwap = ActiveCompareEditorCanSwapContext.bindTo(this.contextKeyService); - this.activeEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.contextKeyService); - this.activeEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.contextKeyService); - this.activeEditorCanSplitInGroup = ActiveEditorCanSplitInGroupContext.bindTo(this.contextKeyService); - this.activeEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.contextKeyService); - this.editorsVisibleContext = EditorsVisibleContext.bindTo(this.contextKeyService); - this.textCompareEditorVisibleContext = TextCompareEditorVisibleContext.bindTo(this.contextKeyService); - this.textCompareEditorActiveContext = TextCompareEditorActiveContext.bindTo(this.contextKeyService); - this.sideBySideEditorActiveContext = SideBySideEditorActiveContext.bindTo(this.contextKeyService); + // Editor Groups this.activeEditorGroupEmpty = ActiveEditorGroupEmptyContext.bindTo(this.contextKeyService); this.activeEditorGroupIndex = ActiveEditorGroupIndexContext.bindTo(this.contextKeyService); this.activeEditorGroupLast = ActiveEditorGroupLastContext.bindTo(this.contextKeyService); this.activeEditorGroupLocked = ActiveEditorGroupLockedContext.bindTo(this.contextKeyService); this.multipleEditorGroupsContext = MultipleEditorGroupsContext.bindTo(this.contextKeyService); + // Editors + this.editorsVisibleContext = EditorsVisibleContext.bindTo(this.contextKeyService); + // Working Copies this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); this.dirtyWorkingCopiesContext.set(this.workingCopyService.hasDirty); @@ -231,18 +203,16 @@ export class WorkbenchContextKeysHandler extends Disposable { private registerListeners(): void { this.editorGroupService.whenReady.then(() => { this.updateEditorAreaContextKeys(); - this.updateEditorContextKeys(); + this.updateActiveEditorGroupContextKeys(); + this.updateVisiblePanesContextKeys(); }); - this._register(this.editorService.onDidActiveEditorChange(() => this.updateEditorContextKeys())); - this._register(this.editorService.onDidVisibleEditorsChange(() => this.updateEditorContextKeys())); - - this._register(this.editorGroupService.onDidAddGroup(() => this.updateEditorContextKeys())); - this._register(this.editorGroupService.onDidRemoveGroup(() => this.updateEditorContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupIndex(() => this.updateEditorContextKeys())); - - this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.updateEditorGroupContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateEditorGroupContextKeys())); + this._register(this.editorService.onDidActiveEditorChange(() => this.updateActiveEditorGroupContextKeys())); + this._register(this.editorService.onDidVisibleEditorsChange(() => this.updateVisiblePanesContextKeys())); + this._register(this.editorGroupService.onDidAddGroup(() => this.updateEditorGroupsContextKeys())); + this._register(this.editorGroupService.onDidRemoveGroup(() => this.updateEditorGroupsContextKeys())); + this._register(this.editorGroupService.onDidChangeGroupIndex(() => this.updateActiveEditorGroupContextKeys())); + this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateActiveEditorGroupContextKeys())); this._register(this.editorGroupService.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); @@ -286,55 +256,32 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.dirtyWorkingCopiesContext.set(workingCopy.isDirty() || this.workingCopyService.hasDirty))); } - private updateEditorAreaContextKeys(): void { - this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); - } - - private updateEditorContextKeys(): void { - const activeEditorPane = this.editorService.activeEditorPane; + private updateVisiblePanesContextKeys(): void { const visibleEditorPanes = this.editorService.visibleEditorPanes; - - this.textCompareEditorActiveContext.set(activeEditorPane?.getId() === TEXT_DIFF_EDITOR_ID); - this.textCompareEditorVisibleContext.set(visibleEditorPanes.some(editorPane => editorPane.getId() === TEXT_DIFF_EDITOR_ID)); - - this.sideBySideEditorActiveContext.set(activeEditorPane?.getId() === SIDE_BY_SIDE_EDITOR_ID); - if (visibleEditorPanes.length > 0) { this.editorsVisibleContext.set(true); } else { this.editorsVisibleContext.reset(); } + } + // Context keys depending on the state of the editor group itself + private updateActiveEditorGroupContextKeys(): void { if (!this.editorService.activeEditor) { this.activeEditorGroupEmpty.set(true); } else { this.activeEditorGroupEmpty.reset(); } - this.updateEditorGroupContextKeys(); + const activeGroup = this.editorGroupService.activeGroup; + this.activeEditorGroupIndex.set(activeGroup.index + 1); // not zero-indexed + this.activeEditorGroupLocked.set(activeGroup.isLocked); - if (activeEditorPane) { - this.activeEditorContext.set(activeEditorPane.getId()); - this.activeEditorCanRevert.set(!activeEditorPane.input.hasCapability(EditorInputCapabilities.Untitled)); - this.activeEditorCanSplitInGroup.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.CanSplitInGroup)); - applyAvailableEditorIds(this.activeEditorAvailableEditorIds, activeEditorPane.input, this.editorResolverService); - this.activeEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); - const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); - const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); - this.activeCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); - this.activeEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); - } else { - this.activeEditorContext.reset(); - this.activeEditorIsReadonly.reset(); - this.activeCompareEditorCanSwap.reset(); - this.activeEditorCanToggleReadonly.reset(); - this.activeEditorCanRevert.reset(); - this.activeEditorCanSplitInGroup.reset(); - this.activeEditorAvailableEditorIds.reset(); - } + this.updateEditorGroupsContextKeys(); } - private updateEditorGroupContextKeys(): void { + // Context keys depending on the state of other editor groups + private updateEditorGroupsContextKeys(): void { const groupCount = this.editorGroupService.count; if (groupCount > 1) { this.multipleEditorGroupsContext.set(true); @@ -343,9 +290,11 @@ export class WorkbenchContextKeysHandler extends Disposable { } const activeGroup = this.editorGroupService.activeGroup; - this.activeEditorGroupIndex.set(activeGroup.index + 1); // not zero-indexed this.activeEditorGroupLast.set(activeGroup.index === groupCount - 1); - this.activeEditorGroupLocked.set(activeGroup.isLocked); + } + + private updateEditorAreaContextKeys(): void { + this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); } private updateInputContextKeys(ownerDocument: Document): void { diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 75213bf3f02..0c9fc4e9e54 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -66,7 +66,7 @@ export interface IResourceLabelOptions extends IIconLabelValueOptions { /** * Uses the provided icon instead of deriving a resource icon. */ - readonly icon?: ThemeIcon; + readonly icon?: ThemeIcon | URI; } export interface IFileLabelOptions extends IResourceLabelOptions { @@ -291,7 +291,7 @@ class ResourceLabelWidget extends IconLabel { readonly onDidRender = this._onDidRender.event; private label: IResourceLabelProps | undefined = undefined; - private decoration = this._register(new MutableDisposable()); + private readonly decoration = this._register(new MutableDisposable()); private options: IResourceLabelOptions | undefined = undefined; private computedIconClasses: string[] | undefined = undefined; @@ -422,7 +422,13 @@ class ResourceLabelWidget extends IconLabel { let description: string | undefined; if (!options?.hidePath) { - description = this.labelService.getUriLabel(dirname(resource), { relative: true }); + const descriptionCandidate = this.labelService.getUriLabel(dirname(resource), { relative: true }); + if (descriptionCandidate && descriptionCandidate !== '.') { + // omit description if its not significant: a relative path + // of '.' just indicates that there is no parent to the path + // https://github.com/microsoft/vscode/issues/208692 + description = descriptionCandidate; + } } this.setResource({ resource, name, description, range: options?.range }, options); @@ -606,6 +612,10 @@ class ResourceLabelWidget extends IconLabel { this.computedIconClasses = getIconClasses(this.modelService, this.languageService, resource, this.options.fileKind, this.options.icon); } + if (URI.isUri(this.options.icon)) { + iconLabelOptions.iconPath = this.options.icon; + } + iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 69fd77ff4ff..4098ce07f8a 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -12,14 +12,14 @@ import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/pl import { EditorInputCapabilities, GroupIdentifier, isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from 'vs/workbench/common/editor'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; -import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar } from 'vs/workbench/services/layout/browser/layoutService'; +import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar, isHorizontal } from 'vs/workbench/services/layout/browser/layoutService'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { StartupKind, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { getMenuBarVisibility, IPath, hasNativeTitlebar, hasCustomTitlebar, TitleBarSetting } from 'vs/platform/window/common/window'; +import { getMenuBarVisibility, IPath, hasNativeTitlebar, hasCustomTitlebar, TitleBarSetting, CustomTitleBarVisibility } from 'vs/platform/window/common/window'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -48,7 +48,6 @@ import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxili import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { CodeWindow, mainWindow } from 'vs/base/browser/window'; -import { CustomTitleBarVisibility } from '../../platform/window/common/window'; //#region Layout Implementation @@ -208,11 +207,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private getContainerDimension(container: HTMLElement): IDimension { if (container === this.mainContainer) { - // main window - return this.mainContainerDimension; + return this.mainContainerDimension; // main window } else { - // auxiliary window - return getClientArea(container); + return getClientArea(container); // auxiliary window } } @@ -427,12 +424,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // The menu bar toggles the title bar in web because it does not need to be shown for window controls only if (isWeb && menuBarVisibility === 'toggle') { - this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled)); + this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive())); } // The menu bar toggles the title bar in full screen for toggle and classic settings else if (this.state.runtime.mainWindowFullscreen && (menuBarVisibility === 'toggle' || menuBarVisibility === 'classic')) { - this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled)); + this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive())); } // Move layout call to any time the menubar @@ -468,8 +465,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.mainContainer.classList.remove(LayoutClasses.FULLSCREEN); const zenModeExitInfo = this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_EXIT_INFO); - const zenModeActive = this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); - if (zenModeExitInfo.transitionedToFullScreen && zenModeActive) { + if (zenModeExitInfo.transitionedToFullScreen && this.isZenModeActive()) { this.toggleZenMode(); } } @@ -482,7 +478,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (hasCustomTitlebar(this.configurationService)) { // Propagate to grid - this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled)); + this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive())); this.updateWindowsBorder(true); } @@ -611,7 +607,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN, false); } - this.stateModel.onDidChangeState(change => { + this._register(this.stateModel.onDidChangeState(change => { if (change.key === LayoutStateKeys.ACTIVITYBAR_HIDDEN) { this.setActivityBarHidden(change.value as boolean); } @@ -633,7 +629,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } this.doUpdateLayoutConfiguration(); - }); + })); // Layout Initialization State const initialEditorsState = this.getInitialEditorsState(); @@ -679,7 +675,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Only restore last viewlet if window was reloaded or we are in development mode let viewContainerToRestore: string | undefined; - if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow) { + if ( + !this.environmentService.isBuilt || + lifecycleService.startupKind === StartupKind.ReloadedWindow || + this.environmentService.isExtensionDevelopment && !this.environmentService.extensionTestsLocationURI + ) { viewContainerToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); } else { viewContainerToRestore = this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; @@ -1072,11 +1072,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi })()); // Restore Zen Mode - const zenModeWasActive = this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); + const zenModeWasActive = this.isZenModeActive(); const restoreZenMode = getZenModeConfiguration(this.configurationService).restore; if (zenModeWasActive) { - this.stateModel.setRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE, !restoreZenMode); + this.setZenModeActive(!restoreZenMode); this.toggleZenMode(false, true); } @@ -1148,6 +1148,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)?.focus(); break; } + case Parts.AUXILIARYBAR_PART: { + this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)?.focus(); + break; + } case Parts.ACTIVITYBAR_PART: (this.getPart(Parts.SIDEBAR_PART) as SidebarPart).focusActivityBar(); break; @@ -1221,7 +1225,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi switch (part) { case Parts.TITLEBAR_PART: - return shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled); + return shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive()); case Parts.SIDEBAR_PART: return !this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN); case Parts.PANEL_PART: @@ -1261,18 +1265,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const containerDimension = this.getContainerDimension(container); if (container === this.mainContainer) { - const panelPosition = this.getPanelPosition(); - const isColumn = panelPosition === Position.RIGHT || panelPosition === Position.LEFT; + const isPanelHorizontal = isHorizontal(this.getPanelPosition()); const takenWidth = (this.isVisible(Parts.ACTIVITYBAR_PART) ? this.activityBarPartView.minimumWidth : 0) + (this.isVisible(Parts.SIDEBAR_PART) ? this.sideBarPartView.minimumWidth : 0) + - (this.isVisible(Parts.PANEL_PART) && isColumn ? this.panelPartView.minimumWidth : 0) + + (this.isVisible(Parts.PANEL_PART) && !isPanelHorizontal ? this.panelPartView.minimumWidth : 0) + (this.isVisible(Parts.AUXILIARYBAR_PART) ? this.auxiliaryBarPartView.minimumWidth : 0); const takenHeight = (this.isVisible(Parts.TITLEBAR_PART, targetWindow) ? this.titleBarPartView.minimumHeight : 0) + (this.isVisible(Parts.STATUSBAR_PART, targetWindow) ? this.statusBarPartView.minimumHeight : 0) + - (this.isVisible(Parts.PANEL_PART) && !isColumn ? this.panelPartView.minimumHeight : 0); + (this.isVisible(Parts.PANEL_PART) && isPanelHorizontal ? this.panelPartView.minimumHeight : 0); const availableWidth = containerDimension.width - takenWidth; const availableHeight = containerDimension.height - takenHeight; @@ -1287,8 +1290,16 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } + private isZenModeActive(): boolean { + return this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); + } + + private setZenModeActive(active: boolean) { + this.stateModel.setRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE, active); + } + toggleZenMode(skipLayout?: boolean, restoring = false): void { - this.stateModel.setRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE, !this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE)); + this.setZenModeActive(!this.isZenModeActive()); this.state.runtime.zenMode.transitionDisposables.clearAndDisposeAll(); const setLineNumbers = (lineNumbers?: LineNumbersType) => { @@ -1314,7 +1325,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const zenModeExitInfo = this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_EXIT_INFO); // Zen Mode Active - if (this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE)) { + if (this.isZenModeActive()) { toggleMainWindowFullScreen = !this.state.runtime.mainWindowFullscreen && config.fullScreen && !isIOS; @@ -1446,7 +1457,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Event - this._onDidChangeZenMode.fire(this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE)); + this._onDidChangeZenMode.fire(this.isZenModeActive()); } private setStatusBarHidden(hidden: boolean, skipLayout?: boolean): void { @@ -1522,28 +1533,29 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi })); } - this._register(this.storageService.onWillSaveState(willSaveState => { - if (willSaveState.reason === WillSaveStateReason.SHUTDOWN) { - // Side Bar Size - const sideBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) - : this.workbenchGrid.getViewSize(this.sideBarPartView).width; - this.stateModel.setInitializationValue(LayoutStateKeys.SIDEBAR_SIZE, sideBarSize as number); + this._register(this.storageService.onWillSaveState(e => { - // Panel Size - const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) - : (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.BOTTOM ? this.workbenchGrid.getViewSize(this.panelPartView).height : this.workbenchGrid.getViewSize(this.panelPartView).width); - this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); + // Side Bar Size + const sideBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) + : this.workbenchGrid.getViewSize(this.sideBarPartView).width; + this.stateModel.setInitializationValue(LayoutStateKeys.SIDEBAR_SIZE, sideBarSize as number); - // Auxiliary Bar Size - const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.auxiliaryBarPartView) - : this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; - this.stateModel.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, auxiliaryBarSize as number); + // Panel Size + const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) + : isHorizontal(this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION)) + ? this.workbenchGrid.getViewSize(this.panelPartView).height + : this.workbenchGrid.getViewSize(this.panelPartView).width; + this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); - this.stateModel.save(true, true); - } + // Auxiliary Bar Size + const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.auxiliaryBarPartView) + : this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; + this.stateModel.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, auxiliaryBarSize as number); + + this.stateModel.save(true, true); })); } @@ -1623,8 +1635,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.resizeView(this.panelPartView, { - width: viewSize.width + (this.getPanelPosition() !== Position.BOTTOM ? sizeChangePxWidth : 0), - height: viewSize.height + (this.getPanelPosition() !== Position.BOTTOM ? 0 : sizeChangePxHeight) + width: viewSize.width + (isHorizontal(this.getPanelPosition()) ? 0 : sizeChangePxWidth), + height: viewSize.height + (isHorizontal(this.getPanelPosition()) ? sizeChangePxHeight : 0) }); break; @@ -1673,7 +1685,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private setActivityBarHidden(hidden: boolean, skipLayout?: boolean): void { this.stateModel.setRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN, hidden); - // Propagate to grid this.workbenchGrid.setViewVisible(this.activityBarPartView, !hidden); } @@ -1759,8 +1770,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private adjustPartPositions(sideBarPosition: Position, panelAlignment: PanelAlignment, panelPosition: Position): void { // Move activity bar and side bars - const sideBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); - const auxiliaryBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); + const isPanelVertical = !isHorizontal(panelPosition); + const sideBarSiblingToEditor = isPanelVertical || !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); + const auxiliaryBarSiblingToEditor = isPanelVertical || !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); const preMovePanelWidth = !this.isVisible(Parts.PANEL_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) ?? this.panelPartView.minimumWidth) : this.workbenchGrid.getViewSize(this.panelPartView).width; const preMovePanelHeight = !this.isVisible(Parts.PANEL_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) ?? this.panelPartView.minimumHeight) : this.workbenchGrid.getViewSize(this.panelPartView).height; const preMoveSideBarSize = !this.isVisible(Parts.SIDEBAR_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) ?? this.sideBarPartView.minimumWidth) : this.workbenchGrid.getViewSize(this.sideBarPartView).width; @@ -1786,7 +1798,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // We moved all the side parts based on the editor and ignored the panel // Now, we need to put the panel back in the right position when it is next to the editor - if (panelPosition !== Position.BOTTOM) { + if (isPanelVertical) { this.workbenchGrid.moveView(this.panelPartView, preMovePanelWidth, this.editorPartView, panelPosition === Position.LEFT ? Direction.Left : Direction.Right); this.workbenchGrid.resizeView(this.panelPartView, { height: preMovePanelHeight as number, @@ -1813,8 +1825,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi setPanelAlignment(alignment: PanelAlignment, skipLayout?: boolean): void { - // Panel alignment only applies to a panel in the bottom position - if (this.getPanelPosition() !== Position.BOTTOM) { + // Panel alignment only applies to a panel in the top/bottom position + if (!isHorizontal(this.getPanelPosition())) { this.setPanelPosition(Position.BOTTOM); } @@ -1910,7 +1922,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const isMaximized = this.isPanelMaximized(); if (!isMaximized) { if (this.isVisible(Parts.PANEL_PART)) { - if (panelPosition === Position.BOTTOM) { + if (isHorizontal(panelPosition)) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT, size.height); } else { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH, size.width); @@ -1921,20 +1933,18 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } else { this.setEditorHidden(false); this.workbenchGrid.resizeView(this.panelPartView, { - width: panelPosition === Position.BOTTOM ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), - height: panelPosition === Position.BOTTOM ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height + width: isHorizontal(panelPosition) ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), + height: isHorizontal(panelPosition) ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height }); } + this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_WAS_LAST_MAXIMIZED, !isMaximized); } - /** - * Returns whether or not the panel opens maximized - */ private panelOpensMaximized(): boolean { // The workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - if (this.getPanelAlignment() !== 'center' && this.getPanelPosition() === Position.BOTTOM) { + if (this.getPanelAlignment() !== 'center' && isHorizontal(this.getPanelPosition())) { return false; } @@ -2012,7 +2022,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi isPanelMaximized(): boolean { // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - return (this.getPanelAlignment() === 'center' || this.getPanelPosition() !== Position.BOTTOM) && !this.isVisible(Parts.EDITOR_PART, mainWindow); + return (this.getPanelAlignment() === 'center' || !isHorizontal(this.getPanelPosition())) && !this.isVisible(Parts.EDITOR_PART, mainWindow); } getSideBarPosition(): Position { @@ -2024,14 +2034,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } updateMenubarVisibility(skipLayout: boolean): void { - const shouldShowTitleBar = shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled); + const shouldShowTitleBar = shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive()); if (!skipLayout && this.workbenchGrid && shouldShowTitleBar !== this.isVisible(Parts.TITLEBAR_PART, mainWindow)) { this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowTitleBar); } } updateCustomTitleBarVisibility(): void { - const shouldShowTitleBar = shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled); + const shouldShowTitleBar = shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive()); const titlebarVisible = this.isVisible(Parts.TITLEBAR_PART); if (shouldShowTitleBar !== titlebarVisible) { this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowTitleBar); @@ -2088,14 +2098,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Save the current size of the panel for the new orthogonal direction // If moving down, save the width of the panel // Otherwise, save the height of the panel - if (position === Position.BOTTOM) { + if (isHorizontal(position)) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH, size.width); - } else if (positionFromString(oldPositionValue) === Position.BOTTOM) { + } else if (isHorizontal(positionFromString(oldPositionValue))) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT, size.height); } } - if (position === Position.BOTTOM && this.getPanelAlignment() !== 'center' && editorHidden) { + if (isHorizontal(position) && this.getPanelAlignment() !== 'center' && editorHidden) { this.toggleMaximizedPanel(); editorHidden = false; } @@ -2107,6 +2117,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (position === Position.BOTTOM) { this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.height : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT), this.editorPartView, Direction.Down); + } else if (position === Position.TOP) { + this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.height : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT), this.editorPartView, Direction.Up); } else if (position === Position.RIGHT) { this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), this.editorPartView, Direction.Right); } else { @@ -2124,7 +2136,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setAuxiliaryBarHidden(true); } - if (position === Position.BOTTOM) { + if (isHorizontal(position)) { this.adjustPartPositions(this.getSideBarPosition(), this.getPanelAlignment(), position); } @@ -2189,7 +2201,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.moveView(this.bannerPartView, Sizing.Distribute, this.titleBarPartView, shouldBannerBeFirst ? Direction.Up : Direction.Down); } - this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled)); + this.workbenchGrid.setViewVisible(this.titleBarPartView, shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive())); } private arrangeEditorNodes(nodes: { editor: ISerializedNode; sideBar?: ISerializedNode; auxiliaryBar?: ISerializedNode }, availableHeight: number, availableWidth: number): ISerializedNode { @@ -2233,17 +2245,20 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) ? 0 : nodes.auxiliaryBar.size; const panelSize = this.stateModel.getInitializationValue(LayoutStateKeys.PANEL_SIZE) ? 0 : nodes.panel.size; + const panelPostion = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION); + const sideBarPosition = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON); + const result = [] as ISerializedNode[]; - if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) !== Position.BOTTOM) { + if (!isHorizontal(panelPostion)) { result.push(nodes.editor); nodes.editor.size = availableWidth - activityBarSize - sideBarSize - panelSize - auxiliaryBarSize; - if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.RIGHT) { + if (panelPostion === Position.RIGHT) { result.push(nodes.panel); } else { result.splice(0, 0, nodes.panel); } - if (this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON) === Position.LEFT) { + if (sideBarPosition === Position.LEFT) { result.push(nodes.auxiliaryBar); result.splice(0, 0, nodes.sideBar); result.splice(0, 0, nodes.activityBar); @@ -2254,18 +2269,20 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } else { const panelAlignment = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_ALIGNMENT); - const sideBarPosition = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON); const sideBarNextToEditor = !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); const auxiliaryBarNextToEditor = !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); const editorSectionWidth = availableWidth - activityBarSize - (sideBarNextToEditor ? 0 : sideBarSize) - (auxiliaryBarNextToEditor ? 0 : auxiliaryBarSize); + + const editorNodes = this.arrangeEditorNodes({ + editor: nodes.editor, + sideBar: sideBarNextToEditor ? nodes.sideBar : undefined, + auxiliaryBar: auxiliaryBarNextToEditor ? nodes.auxiliaryBar : undefined + }, availableHeight - panelSize, editorSectionWidth); + result.push({ type: 'branch', - data: [this.arrangeEditorNodes({ - editor: nodes.editor, - sideBar: sideBarNextToEditor ? nodes.sideBar : undefined, - auxiliaryBar: auxiliaryBarNextToEditor ? nodes.auxiliaryBar : undefined - }, availableHeight - panelSize, editorSectionWidth), nodes.panel], + data: panelPostion === Position.BOTTOM ? [editorNodes, nodes.panel] : [nodes.panel, editorNodes], size: editorSectionWidth }); @@ -2357,7 +2374,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi visible: !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) }; - const middleSection: ISerializedNode[] = this.arrangeMiddleSectionNodes({ activityBar: activityBarNode, auxiliaryBar: auxiliaryBarNode, @@ -2403,13 +2419,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi type StartupLayoutEventClassification = { owner: 'sbatten'; comment: 'Information about the layout of the workbench during statup'; - activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the activity bar is visible' }; - sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the primary side bar is visible' }; - auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the secondary side bar is visible' }; - panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the panel is visible' }; - statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the status bar is visible' }; + activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the activity bar is visible' }; + sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the primary side bar is visible' }; + auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the secondary side bar is visible' }; + panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the panel is visible' }; + statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the status bar is visible' }; sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' }; - panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the bottom, left, or right' }; + panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the top, bottom, left, or right' }; }; const layoutDescriptor: StartupLayoutEvent = { @@ -2565,13 +2581,11 @@ class LayoutStateModel extends Disposable { } private updateStateFromLegacySettings(configurationChangeEvent: IConfigurationChangeEvent): void { - const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); - - if (configurationChangeEvent.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION) && !isZenMode) { + if (configurationChangeEvent.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) { this.setRuntimeValueAndFire(LayoutStateKeys.ACTIVITYBAR_HIDDEN, this.isActivityBarHidden()); } - if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE) && !isZenMode) { + if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)) { this.setRuntimeValueAndFire(LayoutStateKeys.STATUSBAR_HIDDEN, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)); } @@ -2619,7 +2633,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults @@ -2709,6 +2723,7 @@ class LayoutStateModel extends Disposable { if (oldValue !== undefined) { return !oldValue; } + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.DEFAULT; } diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 9299856d6ff..6c9dbb9a0c9 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -87,6 +87,15 @@ body.web { text-decoration: none; } + +.monaco-workbench p > a { + text-decoration: var(--text-link-decoration); +} + +.monaco-workbench.underline-links { + --text-link-decoration: underline; +} + .monaco-workbench.hc-black p > a, .monaco-workbench.hc-light p > a { text-decoration: underline !important; @@ -166,12 +175,15 @@ body.web { } .monaco-workbench .predefined-file-icon[class*='codicon-']::before { - font-family: 'codicon'; width: 16px; padding-left: 3px; /* width (16px) - font-size (13px) = padding-left (3px) */ padding-right: 3px; } +.predefined-file-icon::before { /* do add additional specificity to this selector, so it can be overridden by product themes */ + font-family: 'codicon'; +} + .monaco-workbench:not(.file-icons-enabled) .predefined-file-icon[class*='codicon-']::before { content: unset !important; } @@ -196,8 +208,8 @@ body.web { } .monaco-workbench .select-container:after { - content: "\eab4"; - font-family: codicon; + content: var(--vscode-icon-chevron-down-content); + font-family: var(--vscode-icon-chevron-down-font-family); font-size: 16px; width: 16px; height: 16px; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 15e58a58179..97f3861079f 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -363,10 +363,9 @@ export class ActivityBarCompositeBar extends PaneCompositeBar { } getActivityBarContextMenuActions(): IAction[] { - const activityBarPositionMenu = this.menuService.createMenu(MenuId.ActivityBarPositionMenu, this.contextKeyService); + const activityBarPositionMenu = this.menuService.getMenuActions(MenuId.ActivityBarPositionMenu, this.contextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); const positionActions: IAction[] = []; - createAndFillInContextMenuActions(activityBarPositionMenu, { shouldForwardArgs: true, renderShortTitle: true }, { primary: [], secondary: positionActions }); - activityBarPositionMenu.dispose(); + createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions }); return [ new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions), toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) }) @@ -614,42 +613,24 @@ registerThemingParticipant((theme, collector) => { const outline = theme.getColor(activeContrastBorder); if (outline) { collector.addRule(` - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:before { - content: ""; - position: absolute; - top: 8px; - left: 8px; - height: 32px; - width: 32px; - z-index: 1; + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item .action-label::before{ + padding: 6px; } - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.profile-activity-item:before { - top: -6px; + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active .action-label::before, + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active:hover .action-label::before, + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .action-label::before, + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:hover .action-label::before { + outline: 1px solid ${outline}; } - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active:before, - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active:hover:before, - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:before, - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:hover:before { - outline: 1px solid; - } - - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover:before { - outline: 1px dashed; + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover .action-label::before { + outline: 1px dashed ${outline}; } .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .active-item-indicator:before { border-left-color: ${outline}; } - - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active:before, - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active:hover:before, - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:before, - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:hover:before, - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover:before { - outline-color: ${outline}; - } `); } @@ -658,7 +639,7 @@ registerThemingParticipant((theme, collector) => { const focusBorderColor = theme.getColor(focusBorder); if (focusBorderColor) { collector.addRule(` - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .active-item-indicator:before { + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .active-item-indicator::before { border-left-color: ${focusBorderColor}; } `); diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index a47bbe794a0..b40341d217c 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -26,26 +26,6 @@ display: block; } -.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item::before { - top: 1px; - margin-top: -2px; -} - -.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item::after { - bottom: 1px; - margin-bottom: -2px; -} - -.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { - top: 2px; - margin-top: -2px; -} - -.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { - bottom: 2px; - margin-bottom: -2px; -} - .monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.top::before, .monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.top::after, .monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.bottom::before, @@ -109,7 +89,7 @@ .monaco-workbench.hc-black .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator:before, .monaco-workbench.hc-black .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .active-item-indicator:before { - border-color: var(--vscode-focusBorder); + border-color: var(--vscode-activityBar-activeBorder); } .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator:before { @@ -124,7 +104,8 @@ display: none; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus:before { +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus:before, +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus .active-item-indicator::before { border-left: none !important; /* no focus feedback when using mouse */ } diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 4b6a194ca8d..246ac87fcf6 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -35,6 +35,7 @@ import { $ } from 'vs/base/browser/dom'; import { HiddenItemStrategy, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { CompositeMenuActions } from 'vs/workbench/browser/actions'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export class AuxiliaryBarPart extends AbstractPaneCompositePart { @@ -70,7 +71,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { return Math.max(width, 300); } - readonly priority: LayoutPriority = LayoutPriority.Low; + readonly priority = LayoutPriority.Low; constructor( @INotificationService notificationService: INotificationService, @@ -78,6 +79,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { @IContextMenuService contextMenuService: IContextMenuService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IKeybindingService keybindingService: IKeybindingService, + @IHoverService hoverService: IHoverService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @@ -104,6 +106,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { contextMenuService, layoutService, keybindingService, + hoverService, instantiationService, themeService, viewDescriptorService, @@ -188,10 +191,9 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { actions.push(viewsSubmenuAction); } - const activityBarPositionMenu = this.menuService.createMenu(MenuId.ActivityBarPositionMenu, this.contextKeyService); + const activityBarPositionMenu = this.menuService.getMenuActions(MenuId.ActivityBarPositionMenu, this.contextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); const positionActions: IAction[] = []; - createAndFillInContextMenuActions(activityBarPositionMenu, { shouldForwardArgs: true, renderShortTitle: true }, { primary: [], secondary: positionActions }); - activityBarPositionMenu.dispose(); + createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions }); actions.push(...[ new Separator(), @@ -238,13 +240,6 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { return headerArea; } - protected override getToolbarWidth(): number { - if (this.getCompositeBarPosition() === CompositeBarPosition.TOP) { - return 22; - } - return super.getToolbarWidth(); - } - private headerActionViewItemProvider(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined { if (action.id === ToggleAuxiliaryBarAction.ID) { return this.instantiationService.createInstance(ActionViewItem, undefined, action, options); diff --git a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index 580af0aa4b7..781c090fbe5 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -22,6 +22,10 @@ margin-right: 4px; } +.monaco-workbench .part.auxiliarybar > .title { + background-color: var(--vscode-sideBarTitle-background); +} + .monaco-workbench .part.auxiliarybar > .title > .title-label { flex: 1; } @@ -48,7 +52,7 @@ .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, .monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { position: absolute; - left: 6px; /* place icon in center */ + left: 5px; /* place icon in center */ } .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index dda7c882d5e..e8957eae627 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/bannerpart'; import { localize2 } from 'vs/nls'; -import { $, addDisposableListener, append, asCSSUrl, clearNode, EventType } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, asCSSUrl, clearNode, EventType, isHTMLElement } from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -112,7 +112,7 @@ export class BannerPart extends Part implements IBannerService { if (this.focusedActionIndex < length) { const actionLink = this.messageActionsContainer?.children[this.focusedActionIndex]; - if (actionLink instanceof HTMLElement) { + if (isHTMLElement(actionLink)) { this.actionBar?.setFocusable(false); actionLink.focus(); } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 6ae11c51361..24b1b97b7dd 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -26,7 +26,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { URI } from 'vs/base/common/uri'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IHoverWidget } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import type { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; export interface ICompositeBar { @@ -319,11 +319,7 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { else if (badge instanceof NumberBadge) { if (badge.number) { let number = badge.number.toString(); - if (this.options.compact) { - if (badge.number > 99) { - number = ''; - } - } else if (badge.number > 999) { + if (badge.number > 999) { const noOfThousands = badge.number / 1000; const floor = Math.floor(noOfThousands); if (noOfThousands > floor) { @@ -332,6 +328,9 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { number = `${noOfThousands}K`; } } + if (this.options.compact && number.length >= 3) { + classes.push('compact-content'); + } this.badgeContent.textContent = number; show(this.badge); } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index d1617f55cb5..cbc4783066e 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -35,7 +35,7 @@ import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import type { IHoverService } from 'vs/platform/hover/browser/hover'; export interface ICompositeTitleLabel { @@ -83,6 +83,7 @@ export abstract class CompositePart extends Part { protected readonly contextMenuService: IContextMenuService, layoutService: IWorkbenchLayoutService, protected readonly keybindingService: IKeybindingService, + private readonly hoverService: IHoverService, protected readonly instantiationService: IInstantiationService, themeService: IThemeService, protected readonly registry: CompositeRegistry, @@ -186,9 +187,9 @@ export abstract class CompositePart extends Part { this._register(that.onDidCompositeClose.event(e => this.onScopeClosed(e.getId()))); } }()); - const compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection( + const compositeInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IEditorProgressService, compositeProgressIndicator] // provide the editor progress service for any editors instantiated within the composite - )); + ))); const composite = compositeDescriptor.instantiate(compositeInstantiationService); const disposable = new DisposableStore(); @@ -198,6 +199,7 @@ export abstract class CompositePart extends Part { // Register to title area update events from the composite disposable.add(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this)); + disposable.add(compositeInstantiationService); return composite; } @@ -420,7 +422,7 @@ export abstract class CompositePart extends Part { const titleContainer = append(parent, $('.title-label')); const titleLabel = append(titleContainer, $('h2')); this.titleLabelElement = titleLabel; - const hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), titleLabel, '')); + const hover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), titleLabel, '')); const $this = this; return { diff --git a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index f6f50db5041..279fc5713c4 100644 --- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -121,11 +121,11 @@ export class AuxiliaryEditorPart { const useCustomTitle = isNative && hasCustomTitlebar(this.configurationService); // custom title in aux windows only enabled in native if (useCustomTitle) { titlebarPart = disposables.add(this.titleService.createAuxiliaryTitlebarPart(auxiliaryWindow.container, editorPart)); - titlebarVisible = shouldShowCustomTitleBar(this.configurationService, auxiliaryWindow.window); + titlebarVisible = shouldShowCustomTitleBar(this.configurationService, auxiliaryWindow.window, undefined, false); const handleTitleBarVisibilityEvent = () => { const oldTitlebarPartVisible = titlebarVisible; - titlebarVisible = shouldShowCustomTitleBar(this.configurationService, auxiliaryWindow.window); + titlebarVisible = shouldShowCustomTitleBar(this.configurationService, auxiliaryWindow.window, undefined, false); if (oldTitlebarPartVisible !== titlebarVisible) { updateTitlebarVisibility(true); } @@ -203,10 +203,10 @@ export class AuxiliaryEditorPart { auxiliaryWindow.layout(); // Have a InstantiationService that is scoped to the auxiliary window - const instantiationService = this.instantiationService.createChild(new ServiceCollection( + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection( [IStatusbarService, this.statusbarService.createScoped(statusbarPart, disposables)], [IEditorService, this.editorService.createScoped(editorPart, disposables)] - )); + ))); return { part: editorPart, @@ -279,21 +279,24 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart return; // disabled, auxiliary editor part state is tracked outside } - close(): void { - this.doClose(true /* merge all groups to main part */); + close(): boolean { + return this.doClose(true /* merge all groups to main part */); } - private doClose(mergeGroupsToMainPart: boolean): void { + private doClose(mergeGroupsToMainPart: boolean): boolean { + let result = true; if (mergeGroupsToMainPart) { - this.mergeGroupsToMainPart(); + result = this.mergeGroupsToMainPart(); } this._onWillClose.fire(); + + return result; } - private mergeGroupsToMainPart(): void { + private mergeGroupsToMainPart(): boolean { if (!this.groups.some(group => group.count > 0)) { - return; // skip if we have no editors opened + return true; // skip if we have no editors opened } // Find the most recent group that is not locked @@ -309,7 +312,9 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart targetGroup = this.editorPartsView.mainPart.addGroup(this.editorPartsView.mainPart.activeGroup, this.partOptions.openSideBySideDirection === 'right' ? GroupDirection.RIGHT : GroupDirection.DOWN); } - this.mergeAllGroups(targetGroup); + const result = this.mergeAllGroups(targetGroup); targetGroup.focus(); + + return result; } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index e2fefb174e6..6bba2fcc4c5 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -34,7 +34,6 @@ import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; import { ILabelService } from 'vs/platform/label/common/label'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { IOutline } from 'vs/workbench/services/outline/browser/outline'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; @@ -85,7 +84,7 @@ class OutlineItem extends BreadcrumbsItem { } const template = renderer.renderTemplate(container); - renderer.renderElement(>{ + renderer.renderElement({ element, children: [], depth: 0, diff --git a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts index 7a782b9ee3e..378552f35e0 100644 --- a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -3,15 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { localize2, localize } from 'vs/nls'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { localize, localize2 } from 'vs/nls'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; -import { TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; +import { ActiveCompareEditorCanSwapContext, TextCompareEditorActiveContext, TextCompareEditorVisibleContext } from 'vs/workbench/common/contextkeys'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -31,7 +33,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, primary: KeyMod.Alt | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, true) + handler: (accessor, ...args) => navigateInDiffEditor(accessor, args, true) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -46,7 +48,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, false) + handler: (accessor, ...args) => navigateInDiffEditor(accessor, args, false) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -56,11 +58,12 @@ export function registerDiffEditorCommands(): void { } }); - function getActiveTextDiffEditor(accessor: ServicesAccessor): TextDiffEditor | undefined { + function getActiveTextDiffEditor(accessor: ServicesAccessor, args: any[]): TextDiffEditor | undefined { const editorService = accessor.get(IEditorService); + const resource = args.length > 0 && args[0] instanceof URI ? args[0] : undefined; for (const editor of [editorService.activeEditorPane, ...editorService.visibleEditorPanes]) { - if (editor instanceof TextDiffEditor) { + if (editor instanceof TextDiffEditor && (!resource || editor.input instanceof DiffEditorInput && isEqual(editor.input.primary.resource, resource))) { return editor; } } @@ -68,8 +71,8 @@ export function registerDiffEditorCommands(): void { return undefined; } - function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + function navigateInDiffEditor(accessor: ServicesAccessor, args: any[], next: boolean): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); if (activeTextDiffEditor) { activeTextDiffEditor.getControl()?.goToDiff(next ? 'next' : 'previous'); @@ -82,8 +85,8 @@ export function registerDiffEditorCommands(): void { Toggle } - function focusInDiffEditor(accessor: ServicesAccessor, mode: FocusTextDiffEditorMode): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + function focusInDiffEditor(accessor: ServicesAccessor, args: any[], mode: FocusTextDiffEditorMode): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); if (activeTextDiffEditor) { switch (mode) { @@ -95,32 +98,42 @@ export function registerDiffEditorCommands(): void { break; case FocusTextDiffEditorMode.Toggle: if (activeTextDiffEditor.getControl()?.getModifiedEditor().hasWidgetFocus()) { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original); + return focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Original); } else { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified); + return focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Modified); } } } } - function toggleDiffSideBySide(accessor: ServicesAccessor): void { - const configurationService = accessor.get(IConfigurationService); + function toggleDiffSideBySide(accessor: ServicesAccessor, args: any[]): void { + const configService = accessor.get(ITextResourceConfigurationService); + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); - const newValue = !configurationService.getValue('diffEditor.renderSideBySide'); - configurationService.updateValue('diffEditor.renderSideBySide', newValue); + const m = activeTextDiffEditor?.getControl()?.getModifiedEditor()?.getModel(); + if (!m) { return; } + + const key = 'diffEditor.renderSideBySide'; + const val = configService.getValue(m.uri, key); + configService.updateValue(m.uri, key, !val); } - function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor): void { - const configurationService = accessor.get(IConfigurationService); + function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor, args: any[]): void { + const configService = accessor.get(ITextResourceConfigurationService); + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); - const newValue = !configurationService.getValue('diffEditor.ignoreTrimWhitespace'); - configurationService.updateValue('diffEditor.ignoreTrimWhitespace', newValue); + const m = activeTextDiffEditor?.getControl()?.getModifiedEditor()?.getModel(); + if (!m) { return; } + + const key = 'diffEditor.ignoreTrimWhitespace'; + const val = configService.getValue(m.uri, key); + configService.updateValue(m.uri, key, !val); } - async function swapDiffSides(accessor: ServicesAccessor): Promise { + async function swapDiffSides(accessor: ServicesAccessor, args: any[]): Promise { const editorService = accessor.get(IEditorService); - const diffEditor = getActiveTextDiffEditor(accessor); + const diffEditor = getActiveTextDiffEditor(accessor, args); const activeGroup = diffEditor?.group; const diffInput = diffEditor?.input; if (!diffEditor || typeof activeGroup === 'undefined' || !(diffInput instanceof DiffEditorInput) || !diffInput.modified.resource) { @@ -169,7 +182,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => toggleDiffSideBySide(accessor) + handler: (accessor, ...args) => toggleDiffSideBySide(accessor, args) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -177,7 +190,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Modified) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -185,7 +198,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Original) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -193,7 +206,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Toggle) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Toggle) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -201,7 +214,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => toggleDiffIgnoreTrimWhitespace(accessor) + handler: (accessor, ...args) => toggleDiffIgnoreTrimWhitespace(accessor, args) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -209,7 +222,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => swapDiffSides(accessor) + handler: (accessor, ...args) => swapDiffSides(accessor, args) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 1611076ea51..753f4aa8860 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -11,7 +11,7 @@ import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, EditorPartMultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, EditorTabsVisibleContext, ActiveEditorLastInGroupContext, EditorPartMaximizedEditorGroupContext, MultipleEditorGroupsContext, InEditorZenModeContext, - IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext + IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -69,7 +69,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration'; -import { EditorActionsDefaultAction, EditorActionsTitleBarAction, HideEditorActionsAction, HideEditorTabsAction, ShowMultipleEditorTabsAction, ShowSingleEditorTabAction, ZenHideEditorTabsAction, ZenShowMultipleEditorTabsAction, ZenShowSingleEditorTabAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ConfigureEditorAction, ConfigureEditorTabsAction, EditorActionsDefaultAction, EditorActionsTitleBarAction, HideEditorActionsAction, HideEditorTabsAction, ShowMultipleEditorTabsAction, ShowSingleEditorTabAction, ZenHideEditorTabsAction, ZenShowMultipleEditorTabsAction, ZenShowSingleEditorTabAction } from 'vs/workbench/browser/actions/layoutActions'; import { ICommandAction } from 'vs/platform/action/common/action'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -385,10 +385,12 @@ MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id: EditorActionsTitleBarAction.ID, title: localize('titleBar', "Title Bar"), toggled: ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.editor.editorActionsLocation', 'titleBar'), ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.editor.showTabs', 'none'), ContextKeyExpr.equals('config.workbench.editor.editorActionsLocation', 'default'))) }, group: '1_config', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id: HideEditorActionsAction.ID, title: localize('hidden', "Hidden"), toggled: ContextKeyExpr.equals('config.workbench.editor.editorActionsLocation', 'hidden') }, group: '1_config', order: 30 }); +MenuRegistry.appendMenuItem(MenuId.EditorTabsBarContext, { command: { id: ConfigureEditorTabsAction.ID, title: localize('configureTabs', "Configure Tabs") }, group: '9_configure', order: 10 }); + // Editor Title Context Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITOR_COMMAND_ID, title: localize('close', "Close") }, group: '1_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeOthers', "Close Others"), precondition: EditorGroupEditorsCountContext.notEqualsTo('1') }, group: '1_close', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ActiveEditorLastInGroupContext.toNegated() }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ContextKeyExpr.and(ActiveEditorLastInGroupContext.toNegated(), MultipleEditorsSelectedInGroupContext.negate()) }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_SAVED_EDITORS_COMMAND_ID, title: localize('closeAllSaved', "Close Saved") }, group: '1_close', order: 40 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeAll', "Close All") }, group: '1_close', order: 50 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: REOPEN_WITH_COMMAND_ID, title: localize('reopenWith', "Reopen Editor With...") }, group: '1_open', order: 10, when: ActiveEditorAvailableEditorIdsContext }); @@ -399,10 +401,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_ED MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_DOWN, title: localize('splitDown', "Split Down") }, group: '5_split', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_LEFT, title: localize('splitLeft', "Split Left") }, group: '5_split', order: 30 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_RIGHT, title: localize('splitRight', "Split Right") }, group: '5_split', order: 40 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group") }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group") }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group"), precondition: MultipleEditorsSelectedInGroupContext.negate() }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group"), precondition: MultipleEditorsSelectedInGroupContext.negate() }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('moveToNewWindow', "Move into New Window") }, group: '7_new_window', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('copyToNewWindow', "Copy into New Window") }, group: '7_new_window', order: 20 }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: localize('share', "Share"), group: '11_share', order: -1, when: MultipleEditorsSelectedInGroupContext.negate() }); // Editor Title Menu MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_SIDE_BY_SIDE, title: localize('inlineView', "Inline View"), toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false) }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') }); @@ -413,6 +416,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_KEEP_EDI MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_MAXIMIZE_EDITOR_GROUP, title: localize('maximizeGroup', "Maximize Group") }, group: '8_group_operations', order: 5, when: ContextKeyExpr.and(EditorPartMaximizedEditorGroupContext.negate(), EditorPartMultipleEditorGroupsContext) }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_MAXIMIZE_EDITOR_GROUP, title: localize('unmaximizeGroup', "Unmaximize Group") }, group: '8_group_operations', order: 5, when: EditorPartMaximizedEditorGroupContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_LOCK_GROUP_COMMAND_ID, title: localize('lockGroup', "Lock Group"), toggled: ActiveEditorGroupLockedContext }, group: '8_group_operations', order: 10, when: IsAuxiliaryEditorPartContext.toNegated() /* already a primary action for aux windows */ }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: ConfigureEditorAction.ID, title: localize('configureEditors', "Configure Editors") }, group: '9_configure', order: 10 }); function appendEditorToolItem(primary: ICommandAction, when: ContextKeyExpression | undefined, order: number, alternative?: ICommandAction, precondition?: ContextKeyExpression | undefined): void { const item: IMenuItem = { diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index b118e9f193b..066385368f8 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -18,6 +18,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; import { BooleanVerifier, EnumVerifier, NumberVerifier, ObjectVerifier, SetVerifier, verifyObject } from 'vs/base/common/verifier'; import { IAuxiliaryWindowOpenOptions } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { ContextKeyValue, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export interface IEditorPartCreationOptions { readonly restorePreviousState: boolean; @@ -32,6 +33,7 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { tabActionLocation: 'right', tabActionCloseVisibility: true, tabActionUnpinVisibility: true, + alwaysShowEditorActions: false, tabSizing: 'fit', tabSizingFixedMinWidth: 50, tabSizingFixedMaxWidth: 160, @@ -121,6 +123,7 @@ function validateEditorPartOptions(options: IEditorPartOptions): IEditorPartOpti 'highlightModifiedTabs': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['highlightModifiedTabs']), 'tabActionCloseVisibility': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['tabActionCloseVisibility']), 'tabActionUnpinVisibility': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['tabActionUnpinVisibility']), + 'alwaysShowEditorActions': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['alwaysShowEditorActions']), 'pinnedTabsOnSeparateRow': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['pinnedTabsOnSeparateRow']), 'focusRecentEditorAfterClose': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['focusRecentEditorAfterClose']), 'showIcons': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['showIcons']), @@ -185,6 +188,8 @@ export interface IEditorPartsView { readonly count: number; createAuxiliaryEditorPart(options?: IAuxiliaryWindowOpenOptions): Promise; + + bind(contextKey: RawContextKey, group: IEditorGroupView): IContextKey; } /** @@ -209,7 +214,7 @@ export interface IEditorGroupsView { restoreGroup(identifier: IEditorGroupView | GroupIdentifier): IEditorGroupView; addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, groupToCopy?: IEditorGroupView): IEditorGroupView; - mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroupView; + mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): boolean; moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView; copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView; @@ -237,6 +242,15 @@ export interface IEditorGroupTitleHeight { readonly offset: number; } +export interface IEditorGroupViewOptions { + + /** + * Whether the editor group should receive keyboard focus + * after creation or not. + */ + readonly preserveFocus?: boolean; +} + /** * A helper to access and mutate an editor group within an editor part. */ @@ -333,6 +347,11 @@ export interface IInternalEditorOpenOptions extends IInternalEditorTitleControlO * the top that the editor opens in. */ readonly preserveWindowOrder?: boolean; + + /** + * Inactive editors to select after opening the active selected editor. + */ + readonly inactiveSelection?: EditorInput[]; } export interface IInternalEditorCloseOptions extends IInternalEditorTitleControlOptions { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index b02d66f3cf4..3293be1a2c2 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -13,7 +13,7 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { GoFilter, IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, resolveCommandsContext, getCommandsContext, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -34,9 +34,11 @@ import { IKeybindingRule, KeybindingWeight } from 'vs/platform/keybinding/common import { ILogService } from 'vs/platform/log/common/log'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEditorGroupEmptyContext, AuxiliaryBarVisibleContext, EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, MultipleEditorGroupsContext, SideBarVisibleContext } from 'vs/workbench/common/contextkeys'; -import { URI } from 'vs/base/common/uri'; import { getActiveDocument } from 'vs/base/browser/dom'; import { ICommandActionTitle } from 'vs/platform/action/common/action'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; +import { IListService } from 'vs/platform/list/browser/listService'; class ExecuteCommandAction extends Action2 { @@ -61,11 +63,14 @@ abstract class AbstractSplitEditorAction extends Action2 { return preferredSideBySideGroupDirection(configurationService); } - override async run(accessor: ServicesAccessor, context?: IEditorIdentifier): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - splitEditor(editorGroupService, this.getDirection(configurationService), context); + const direction = this.getDirection(configurationService); + const commandContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); + + splitEditor(editorGroupsService, direction, commandContext); } } @@ -436,7 +441,7 @@ export class UnpinEditorAction extends Action { } } -export class CloseOneEditorAction extends Action { +export class CloseEditorTabAction extends Action { static readonly ID = 'workbench.action.closeActiveEditor'; static readonly LABEL = localize('closeOneEditor', "Close"); @@ -450,34 +455,29 @@ export class CloseOneEditorAction extends Action { } override async run(context?: IEditorCommandsContext): Promise { - let group: IEditorGroup | undefined; - let editorIndex: number | undefined; - if (context) { - group = this.editorGroupService.getGroup(context.groupId); - - if (group) { - editorIndex = context.editorIndex; // only allow editor at index if group is valid - } - } - + const group = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup; if (!group) { - group = this.editorGroupService.activeGroup; - } - - // Close specific editor in group - if (typeof editorIndex === 'number') { - const editorAtIndex = group.getEditorByIndex(editorIndex); - if (editorAtIndex) { - await group.closeEditor(editorAtIndex, { preserveFocus: context?.preserveFocus }); - return; - } - } - - // Otherwise close active editor in group - if (group.activeEditor) { - await group.closeEditor(group.activeEditor, { preserveFocus: context?.preserveFocus }); + // group mentioned in context does not exist return; } + + const targetEditor = context?.editorIndex !== undefined ? group.getEditorByIndex(context.editorIndex) : group.activeEditor; + if (!targetEditor) { + // No editor open or editor at index does not exist + return; + } + + const editors: EditorInput[] = []; + if (group.isSelected(targetEditor)) { + editors.push(...group.selectedEditors); + } else { + editors.push(targetEditor); + } + + // Close specific editors in group + for (const editor of editors) { + await group.closeEditor(editor, { preserveFocus: context?.preserveFocus }); + } } } @@ -568,6 +568,8 @@ abstract class AbstractCloseAllAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + const progressService = accessor.get(IProgressService); const editorGroupService = accessor.get(IEditorGroupsService); const filesConfigurationService = accessor.get(IFilesConfigurationService); const fileDialogService = accessor.get(IFileDialogService); @@ -640,7 +642,7 @@ abstract class AbstractCloseAllAction extends Action2 { case ConfirmResult.CANCEL: return; case ConfirmResult.DONT_SAVE: - await editorService.revert(editors, { soft: true }); + await this.revertEditors(editorService, logService, progressService, editors); break; case ConfirmResult.SAVE: await editorService.save(editors, { reason: SaveReason.EXPLICIT }); @@ -660,7 +662,7 @@ abstract class AbstractCloseAllAction extends Action2 { case ConfirmResult.CANCEL: return; case ConfirmResult.DONT_SAVE: - await editorService.revert(editors, { soft: true }); + await this.revertEditors(editorService, logService, progressService, editors); break; case ConfirmResult.SAVE: await editorService.save(editors, { reason: SaveReason.EXPLICIT }); @@ -690,6 +692,33 @@ abstract class AbstractCloseAllAction extends Action2 { return this.doCloseAll(editorGroupService); } + private revertEditors(editorService: IEditorService, logService: ILogService, progressService: IProgressService, editors: IEditorIdentifier[]): Promise { + return progressService.withProgress({ + location: ProgressLocation.Window, // use window progress to not be too annoying about this operation + delay: 800, // delay so that it only appears when operation takes a long time + title: localize('reverting', "Reverting Editors..."), + }, () => this.doRevertEditors(editorService, logService, editors)); + } + + private async doRevertEditors(editorService: IEditorService, logService: ILogService, editors: IEditorIdentifier[]): Promise { + try { + // We first attempt to revert all editors with `soft: false`, to ensure that + // working copies revert to their state on disk. Even though we close editors, + // it is possible that other parties hold a reference to the working copy + // and expect it to be in a certain state after the editor is closed without + // saving. + await editorService.revert(editors); + } catch (error) { + logService.error(error); + + // if that fails, since we are about to close the editor, we accept that + // the editor cannot be reverted and instead do a soft revert that just + // enables us to close the editor. With this, a user can always close a + // dirty editor even when reverting fails. + await editorService.revert(editors, { soft: true }); + } + } + private async revealEditorsToConfirm(editors: ReadonlyArray, editorGroupService: IEditorGroupsService): Promise { try { const handledGroups = new Set(); @@ -1143,11 +1172,13 @@ export class ToggleMaximizeEditorGroupAction extends Action2 { }); } - override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { const editorGroupsService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupsService, getCommandsContext(resourceOrContext, context)); - editorGroupsService.toggleMaximizeGroup(group); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); + if (resolvedContext.groupedEditors.length) { + editorGroupsService.toggleMaximizeGroup(resolvedContext.groupedEditors[0].group); + } } } @@ -1965,7 +1996,7 @@ export class MoveEditorLeftInGroupAction extends ExecuteCommandAction { }, f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left' } satisfies ActiveEditorMoveCopyArguments); } } @@ -1984,7 +2015,7 @@ export class MoveEditorRightInGroupAction extends ExecuteCommandAction { }, f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2003,7 +2034,7 @@ export class MoveEditorToPreviousGroupAction extends ExecuteCommandAction { }, f1: true, category: Categories.View, - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2022,7 +2053,7 @@ export class MoveEditorToNextGroupAction extends ExecuteCommandAction { } }, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2034,7 +2065,7 @@ export class MoveEditorToAboveGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToAboveGroup', 'Move Editor into Group Above'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2046,7 +2077,7 @@ export class MoveEditorToBelowGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToBelowGroup', 'Move Editor into Group Below'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2058,7 +2089,7 @@ export class MoveEditorToLeftGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToLeftGroup', 'Move Editor into Left Group'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2070,7 +2101,7 @@ export class MoveEditorToRightGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToRightGroup', 'Move Editor into Right Group'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2089,7 +2120,7 @@ export class MoveEditorToFirstGroupAction extends ExecuteCommandAction { } }, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2108,7 +2139,7 @@ export class MoveEditorToLastGroupAction extends ExecuteCommandAction { } }, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } as ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2120,7 +2151,7 @@ export class SplitEditorToPreviousGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToPreviousGroup', 'Split Editor into Previous Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2132,7 +2163,7 @@ export class SplitEditorToNextGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToNextGroup', 'Split Editor into Next Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2144,7 +2175,7 @@ export class SplitEditorToAboveGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToAboveGroup', 'Split Editor into Group Above'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2156,7 +2187,7 @@ export class SplitEditorToBelowGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToBelowGroup', 'Split Editor into Group Below'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2171,7 +2202,7 @@ export class SplitEditorToLeftGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToLeftGroup', "Split Editor into Left Group"), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2183,7 +2214,7 @@ export class SplitEditorToRightGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToRightGroup', 'Split Editor into Right Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2195,7 +2226,7 @@ export class SplitEditorToFirstGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToFirstGroup', 'Split Editor into First Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2207,7 +2238,7 @@ export class SplitEditorToLastGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToLastGroup', 'Split Editor into Last Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } as ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } satisfies ActiveEditorMoveCopyArguments); } } @@ -2221,7 +2252,7 @@ export class EditorLayoutSingleAction extends ExecuteCommandAction { title: localize2('editorLayoutSingle', 'Single Column Editor Layout'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}] } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}], orientation: GroupOrientation.HORIZONTAL } satisfies EditorGroupLayout); } } @@ -2235,7 +2266,7 @@ export class EditorLayoutTwoColumnsAction extends ExecuteCommandAction { title: localize2('editorLayoutTwoColumns', 'Two Columns Editor Layout'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}], orientation: GroupOrientation.HORIZONTAL } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}], orientation: GroupOrientation.HORIZONTAL } satisfies EditorGroupLayout); } } @@ -2249,7 +2280,7 @@ export class EditorLayoutThreeColumnsAction extends ExecuteCommandAction { title: localize2('editorLayoutThreeColumns', 'Three Columns Editor Layout'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}, {}], orientation: GroupOrientation.HORIZONTAL } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}, {}], orientation: GroupOrientation.HORIZONTAL } satisfies EditorGroupLayout); } } @@ -2263,7 +2294,7 @@ export class EditorLayoutTwoRowsAction extends ExecuteCommandAction { title: localize2('editorLayoutTwoRows', 'Two Rows Editor Layout'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}], orientation: GroupOrientation.VERTICAL } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}], orientation: GroupOrientation.VERTICAL } satisfies EditorGroupLayout); } } @@ -2277,7 +2308,7 @@ export class EditorLayoutThreeRowsAction extends ExecuteCommandAction { title: localize2('editorLayoutThreeRows', 'Three Rows Editor Layout'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}, {}], orientation: GroupOrientation.VERTICAL } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, {}, {}], orientation: GroupOrientation.VERTICAL } satisfies EditorGroupLayout); } } @@ -2291,7 +2322,7 @@ export class EditorLayoutTwoByTwoGridAction extends ExecuteCommandAction { title: localize2('editorLayoutTwoByTwoGrid', 'Grid Editor Layout (2x2)'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{ groups: [{}, {}] }, { groups: [{}, {}] }] } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{ groups: [{}, {}] }, { groups: [{}, {}] }], orientation: GroupOrientation.HORIZONTAL } satisfies EditorGroupLayout); } } @@ -2305,7 +2336,7 @@ export class EditorLayoutTwoColumnsBottomAction extends ExecuteCommandAction { title: localize2('editorLayoutTwoColumnsBottom', 'Two Columns Bottom Editor Layout'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, { groups: [{}, {}] }], orientation: GroupOrientation.VERTICAL } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, { groups: [{}, {}] }], orientation: GroupOrientation.VERTICAL } satisfies EditorGroupLayout); } } @@ -2319,7 +2350,7 @@ export class EditorLayoutTwoRowsRightAction extends ExecuteCommandAction { title: localize2('editorLayoutTwoRowsRight', 'Two Rows Right Editor Layout'), f1: true, category: Categories.View - }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, { groups: [{}, {}] }], orientation: GroupOrientation.HORIZONTAL } as EditorGroupLayout); + }, LAYOUT_EDITOR_GROUPS_COMMAND_ID, { groups: [{}, { groups: [{}, {}] }], orientation: GroupOrientation.HORIZONTAL } satisfies EditorGroupLayout); } } @@ -2512,21 +2543,27 @@ abstract class BaseMoveCopyEditorToNewWindowAction extends Action2 { }); } - override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { + override async run(accessor: ServicesAccessor, ...args: unknown[]) { const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart(); - - if (this.move) { - group.moveEditor(editor, auxiliaryEditorPart.activeGroup); - } else { - group.copyEditor(editor, auxiliaryEditorPart.activeGroup); - } - - auxiliaryEditorPart.activeGroup.focus(); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupService, accessor.get(IListService)); + if (!resolvedContext.groupedEditors.length) { + return; } + + const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart(); + + // only single group supported for move/copy for now + const { group, editors } = resolvedContext.groupedEditors[0]; + const options = { preserveFocus: resolvedContext.preserveFocus }; + const editorsWithOptions = editors.map(editor => ({ editor, options })); + + if (this.move) { + group.moveEditors(editorsWithOptions, auxiliaryEditorPart.activeGroup); + } else { + group.copyEditors(editorsWithOptions, auxiliaryEditorPart.activeGroup); + } + + auxiliaryEditorPart.activeGroup.focus(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index 66940e29b77..2b3de674cb9 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -29,7 +29,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Auto save: focus change & window change private lastActiveEditor: EditorInput | undefined = undefined; private lastActiveGroupId: GroupIdentifier | undefined = undefined; - private lastActiveEditorControlDisposable = this._register(new DisposableStore()); + private readonly lastActiveEditorControlDisposable = this._register(new DisposableStore()); // Auto save: waiting on specific condition private readonly waitingOnConditionAutoSaveWorkingCopies = new ResourceMap<{ readonly workingCopy: IWorkingCopy; readonly reason: SaveReason; condition: AutoSaveDisabledReason }>(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 89795c71057..10405ef3d50 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -3,9 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveElement } from 'vs/base/browser/dom'; -import { List } from 'vs/base/browser/ui/list/listWidget'; -import { coalesce, distinct } from 'vs/base/common/arrays'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas, matchesScheme } from 'vs/base/common/network'; @@ -31,17 +28,18 @@ import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/br import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; -import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { CloseDirection, EditorInputCapabilities, EditorsOrder, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { EditorGroupColumn, columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, isEditorGroup, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, IEditorReplacement, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { DIFF_FOCUS_OTHER_SIDE, DIFF_FOCUS_PRIMARY_SIDE, DIFF_FOCUS_SECONDARY_SIDE, DIFF_OPEN_SIDE, registerDiffEditorCommands } from './diffEditorCommands'; +import { IResolvedEditorCommandsContext, resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors'; export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup'; @@ -111,9 +109,9 @@ export const EDITOR_CORE_NAVIGATION_COMMANDS = [ ]; export interface ActiveEditorMoveCopyArguments { - to: 'first' | 'last' | 'left' | 'right' | 'up' | 'down' | 'center' | 'position' | 'previous' | 'next'; - by: 'tab' | 'group'; - value: number; + to?: 'first' | 'last' | 'left' | 'right' | 'up' | 'down' | 'center' | 'position' | 'previous' | 'next'; + by?: 'tab' | 'group'; + value?: number; } const isActiveEditorMoveCopyArg = function (arg: ActiveEditorMoveCopyArguments): boolean { @@ -224,16 +222,16 @@ function registerActiveEditorMoveCopyCommand(): void { index = group.count - 1; break; case 'left': - index = index - args.value; + index = index - (args.value ?? 1); break; case 'right': - index = index + args.value; + index = index + (args.value ?? 1); break; case 'center': index = Math.round(group.count / 2) - 1; break; case 'position': - index = args.value - 1; + index = (args.value ?? 1) - 1; break; } @@ -242,7 +240,7 @@ function registerActiveEditorMoveCopyCommand(): void { } function moveCopyActiveEditorToGroup(isMove: boolean, args: ActiveEditorMoveCopyArguments, control: IVisibleEditorPane, accessor: ServicesAccessor): void { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); const sourceGroup = control.group; @@ -250,49 +248,49 @@ function registerActiveEditorMoveCopyCommand(): void { switch (args.to) { case 'left': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.LEFT }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.LEFT }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.LEFT); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.LEFT); } break; case 'right': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.RIGHT }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.RIGHT }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.RIGHT); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.RIGHT); } break; case 'up': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.UP }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.UP }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.UP); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.UP); } break; case 'down': - targetGroup = editorGroupService.findGroup({ direction: GroupDirection.DOWN }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ direction: GroupDirection.DOWN }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, GroupDirection.DOWN); + targetGroup = editorGroupsService.addGroup(sourceGroup, GroupDirection.DOWN); } break; case 'first': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.FIRST }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.FIRST }, sourceGroup); break; case 'last': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.LAST }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.LAST }, sourceGroup); break; case 'previous': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.PREVIOUS }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.PREVIOUS }, sourceGroup); break; case 'next': - targetGroup = editorGroupService.findGroup({ location: GroupLocation.NEXT }, sourceGroup); + targetGroup = editorGroupsService.findGroup({ location: GroupLocation.NEXT }, sourceGroup); if (!targetGroup) { - targetGroup = editorGroupService.addGroup(sourceGroup, preferredSideBySideGroupDirection(configurationService)); + targetGroup = editorGroupsService.addGroup(sourceGroup, preferredSideBySideGroupDirection(configurationService)); } break; case 'center': - targetGroup = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)[(editorGroupService.count / 2) - 1]; + targetGroup = editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE)[(editorGroupsService.count / 2) - 1]; break; case 'position': - targetGroup = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)[args.value - 1]; + targetGroup = editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE)[(args.value ?? 1) - 1]; break; } @@ -314,8 +312,8 @@ function registerEditorGroupsLayoutCommands(): void { return; } - const editorGroupService = accessor.get(IEditorGroupsService); - editorGroupService.applyLayout(layout); + const editorGroupsService = accessor.get(IEditorGroupsService); + editorGroupsService.applyLayout(layout); } CommandsRegistry.registerCommand(LAYOUT_EDITOR_GROUPS_COMMAND_ID, (accessor: ServicesAccessor, args: EditorGroupLayout) => { @@ -352,9 +350,9 @@ function registerEditorGroupsLayoutCommands(): void { CommandsRegistry.registerCommand({ id: 'vscode.getEditorLayout', handler: (accessor: ServicesAccessor) => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); - return editorGroupService.getLayout(); + return editorGroupsService.getLayout(); }, metadata: { description: 'Get Editor Layout', @@ -392,7 +390,7 @@ function registerOpenEditorAPICommands(): void { CommandsRegistry.registerCommand(API_OPEN_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, resourceArg: UriComponents | string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], label?: string, context?: IOpenEvent) { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const openerService = accessor.get(IOpenerService); const pathService = accessor.get(IPathService); const configurationService = accessor.get(IConfigurationService); @@ -421,7 +419,7 @@ function registerOpenEditorAPICommands(): void { input = { resource, options, label }; } - await editorService.openEditor(input, columnToEditorGroup(editorGroupService, configurationService, column)); + await editorService.openEditor(input, columnToEditorGroup(editorGroupsService, configurationService, column)); } // do not allow to execute commands from here @@ -454,7 +452,7 @@ function registerOpenEditorAPICommands(): void { CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, originalResource: UriComponents, modifiedResource: UriComponents, labelAndOrDescription?: string | { label: string; description: string }, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); const [columnArg, optionsArg] = columnAndOptions ?? []; @@ -475,7 +473,7 @@ function registerOpenEditorAPICommands(): void { label, description, options - }, columnToEditorGroup(editorGroupService, configurationService, column)); + }, columnToEditorGroup(editorGroupsService, configurationService, column)); }); CommandsRegistry.registerCommand(API_OPEN_WITH_EDITOR_COMMAND_ID, async (accessor: ServicesAccessor, resource: UriComponents, id: string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?]) => { @@ -596,30 +594,30 @@ function registerFocusEditorGroupAtIndexCommands(): void { when: undefined, primary: KeyMod.CtrlCmd | toKeyCode(groupIndex), handler: accessor => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); // To keep backwards compatibility (pre-grid), allow to focus a group // that does not exist as long as it is the next group after the last // opened group. Otherwise we return. - if (groupIndex > editorGroupService.count) { + if (groupIndex > editorGroupsService.count) { return; } // Group exists: just focus - const groups = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE); + const groups = editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE); if (groups[groupIndex]) { return groups[groupIndex].focus(); } // Group does not exist: create new by splitting the active one of the last group const direction = preferredSideBySideGroupDirection(configurationService); - const lastGroup = editorGroupService.findGroup({ location: GroupLocation.LAST }); + const lastGroup = editorGroupsService.findGroup({ location: GroupLocation.LAST }); if (!lastGroup) { return; } - const newGroup = editorGroupService.addGroup(lastGroup, direction); + const newGroup = editorGroupsService.addGroup(lastGroup, direction); // Focus newGroup.focus(); @@ -656,32 +654,21 @@ function registerFocusEditorGroupAtIndexCommands(): void { } } -export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, context?: IEditorCommandsContext): void { - let sourceGroup: IEditorGroup | undefined; - if (context && typeof context.groupId === 'number') { - sourceGroup = editorGroupService.getGroup(context.groupId); - } else { - sourceGroup = editorGroupService.activeGroup; - } - - if (!sourceGroup) { +export function splitEditor(editorGroupsService: IEditorGroupsService, direction: GroupDirection, resolvedContext: IResolvedEditorCommandsContext): void { + if (!resolvedContext.groupedEditors.length) { return; } - // Add group - const newGroup = editorGroupService.addGroup(sourceGroup, direction); + // Only support splitting from one source group + const { group, editors } = resolvedContext.groupedEditors[0]; + const preserveFocus = resolvedContext.preserveFocus; + const newGroup = editorGroupsService.addGroup(group, direction); - // Split editor (if it can be split) - let editorToCopy: EditorInput | undefined; - if (context && typeof context.editorIndex === 'number') { - editorToCopy = sourceGroup.getEditorByIndex(context.editorIndex); - } else { - editorToCopy = sourceGroup.activeEditor ?? undefined; - } - - // Copy the editor to the new group, else create an empty group - if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { - sourceGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: context?.preserveFocus }); + for (const editorToCopy of editors) { + // Split editor (if it can be split) + if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { + group.copyEditor(editorToCopy, newGroup, { preserveFocus }); + } } // Focus @@ -695,8 +682,9 @@ function registerSplitEditorCommands() { { id: SPLIT_EDITOR_LEFT, direction: GroupDirection.LEFT }, { id: SPLIT_EDITOR_RIGHT, direction: GroupDirection.RIGHT } ].forEach(({ id, direction }) => { - CommandsRegistry.registerCommand(id, function (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { - splitEditor(accessor.get(IEditorGroupsService), direction, getCommandsContext(resourceOrContext, context)); + CommandsRegistry.registerCommand(id, function (accessor, ...args) { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + splitEditor(accessor.get(IEditorGroupsService), direction, resolvedContext); }); }); } @@ -706,14 +694,14 @@ function registerCloseEditorCommands() { // A special handler for "Close Editor" depending on context // - keybindining: do not close sticky editors, rather open the next non-sticky editor // - menu: always close editor, even sticky ones - function closeEditorHandler(accessor: ServicesAccessor, forceCloseStickyEditors: boolean, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { + function closeEditorHandler(accessor: ServicesAccessor, forceCloseStickyEditors: boolean, ...args: unknown[]): Promise { const editorGroupsService = accessor.get(IEditorGroupsService); const editorService = accessor.get(IEditorService); let keepStickyEditors: boolean | undefined = undefined; if (forceCloseStickyEditors) { keepStickyEditors = false; // explicitly close sticky editors - } else if (resourceOrContext || context) { + } else if (args.length) { keepStickyEditors = false; // we have a context, as such this command was used e.g. from the tab context menu } else { keepStickyEditors = editorGroupsService.partOptions.preventPinnedEditorClose === 'keyboard' || editorGroupsService.partOptions.preventPinnedEditorClose === 'keyboardAndMouse'; // respect setting otherwise @@ -741,17 +729,12 @@ function registerCloseEditorCommands() { } // With context: proceed to close editors as instructed - const { editors, groups } = getEditorsContext(accessor, resourceOrContext, context); + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + const preserveFocus = resolvedContext.preserveFocus; - return Promise.all(groups.map(async group => { - if (group) { - const editorsToClose = coalesce(editors - .filter(editor => editor.groupId === group.id) - .map(editor => typeof editor.editorIndex === 'number' ? group.getEditorByIndex(editor.editorIndex) : group.activeEditor)) - .filter(editor => !keepStickyEditors || !group.isSticky(editor)); - - await group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); - } + return Promise.all(resolvedContext.groupedEditors.map(async ({ group, editors }) => { + const editorsToClose = editors.filter(editor => !keepStickyEditors || !group.isSticky(editor)); + await group.closeEditors(editorsToClose, { preserveFocus }); })); } @@ -761,13 +744,13 @@ function registerCloseEditorCommands() { when: undefined, primary: KeyMod.CtrlCmd | KeyCode.KeyW, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return closeEditorHandler(accessor, false, resourceOrContext, context); + handler: (accessor, ...args: unknown[]) => { + return closeEditorHandler(accessor, false, ...args); } }); - CommandsRegistry.registerCommand(CLOSE_PINNED_EDITOR_COMMAND_ID, (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return closeEditorHandler(accessor, true /* force close pinned editors */, resourceOrContext, context); + CommandsRegistry.registerCommand(CLOSE_PINNED_EDITOR_COMMAND_ID, (accessor, ...args: unknown[]) => { + return closeEditorHandler(accessor, true /* force close pinned editors */, ...args); }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -775,12 +758,10 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyW), - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return Promise.all(getEditorsContext(accessor, resourceOrContext, context).groups.map(async group => { - if (group) { - await group.closeAllEditors({ excludeSticky: true }); - return; - } + handler: (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + return Promise.all(resolvedContext.groupedEditors.map(async ({ group }) => { + await group.closeAllEditors({ excludeSticky: true }); })); } }); @@ -791,19 +772,12 @@ function registerCloseEditorCommands() { when: ContextKeyExpr.and(ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext), primary: KeyMod.CtrlCmd | KeyCode.KeyW, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - const commandsContext = getCommandsContext(resourceOrContext, context); + handler: (accessor, ...args: unknown[]) => { + const editorGroupsService = accessor.get(IEditorGroupsService); + const commandsContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); - let group: IEditorGroup | undefined; - if (commandsContext && typeof commandsContext.groupId === 'number') { - group = editorGroupService.getGroup(commandsContext.groupId); - } else { - group = editorGroupService.activeGroup; - } - - if (group) { - editorGroupService.removeGroup(group); + if (commandsContext.groupedEditors.length) { + editorGroupsService.removeGroup(commandsContext.groupedEditors[0].group); } } }); @@ -813,11 +787,10 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyU), - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return Promise.all(getEditorsContext(accessor, resourceOrContext, context).groups.map(async group => { - if (group) { - await group.closeEditors({ savedOnly: true, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); - } + handler: (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + return Promise.all(resolvedContext.groupedEditors.map(async ({ group }) => { + await group.closeEditors({ savedOnly: true, excludeSticky: true }, { preserveFocus: resolvedContext.preserveFocus }); })); } }); @@ -828,24 +801,19 @@ function registerCloseEditorCommands() { when: undefined, primary: undefined, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyT }, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const { editors, groups } = getEditorsContext(accessor, resourceOrContext, context); - return Promise.all(groups.map(async group => { - if (group) { - const editorsToKeep = editors - .filter(editor => editor.groupId === group.id) - .map(editor => typeof editor.editorIndex === 'number' ? group.getEditorByIndex(editor.editorIndex) : group.activeEditor); + handler: (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); - const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => !editorsToKeep.includes(editor)); + return Promise.all(resolvedContext.groupedEditors.map(async ({ group, editors }) => { + const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => !editors.includes(editor)); - for (const editorToKeep of editorsToKeep) { - if (editorToKeep) { - group.pinEditor(editorToKeep); - } + for (const editorToKeep of editors) { + if (editorToKeep) { + group.pinEditor(editorToKeep); } - - await group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); } + + await group.closeEditors(editorsToClose, { preserveFocus: resolvedContext.preserveFocus }); })); } }); @@ -855,16 +823,15 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + if (resolvedContext.groupedEditors.length) { + const { group, editors } = resolvedContext.groupedEditors[0]; if (group.activeEditor) { group.pinEditor(group.activeEditor); } - await group.closeEditors({ direction: CloseDirection.RIGHT, except: editor, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); + await group.closeEditors({ direction: CloseDirection.RIGHT, except: editors[0], excludeSticky: true }, { preserveFocus: resolvedContext.preserveFocus }); } } }); @@ -874,76 +841,84 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); + handler: async (accessor, ...args: unknown[]) => { const editorService = accessor.get(IEditorService); const editorResolverService = accessor.get(IEditorResolverService); const telemetryService = accessor.get(ITelemetryService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); + const editorReplacements = new Map(); - if (!editor) { - return; - } - const untypedEditor = editor.toUntyped(); + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + const untypedEditor = editor.toUntyped(); + if (!untypedEditor) { + return; // Resolver can only resolve untyped editors + } - // Resolver can only resolve untyped editors - if (!untypedEditor) { - return; - } - untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; - const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); - if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { - return; - } + untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; + const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); + if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { + return; + } - // Replace editor with resolved one - await resolvedEditor.group.replaceEditors([ - { - editor: editor, - replacement: resolvedEditor.editor, - forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, - options: resolvedEditor.options + let editorReplacementsInGroup = editorReplacements.get(group); + if (!editorReplacementsInGroup) { + editorReplacementsInGroup = []; + editorReplacements.set(group, editorReplacementsInGroup); + } + + editorReplacementsInGroup.push({ + editor: editor, + replacement: resolvedEditor.editor, + forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, + options: resolvedEditor.options + }); + + // Telemetry + type WorkbenchEditorReopenClassification = { + owner: 'rebornix'; + comment: 'Identify how a document is reopened'; + scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; + ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; + from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; + to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; + }; + + type WorkbenchEditorReopenEvent = { + scheme: string; + ext: string; + from: string; + to: string; + }; + + telemetryService.publicLog2('workbenchEditorReopen', { + scheme: editor.resource?.scheme ?? '', + ext: editor.resource ? extname(editor.resource) : '', + from: editor.editorId ?? '', + to: resolvedEditor.editor.editorId ?? '' + }); } - ]); + } - type WorkbenchEditorReopenClassification = { - owner: 'rebornix'; - comment: 'Identify how a document is reopened'; - scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; - ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; - from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; - to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; - }; - - type WorkbenchEditorReopenEvent = { - scheme: string; - ext: string; - from: string; - to: string; - }; - - telemetryService.publicLog2('workbenchEditorReopen', { - scheme: editor.resource?.scheme ?? '', - ext: editor.resource ? extname(editor.resource) : '', - from: editor.editorId ?? '', - to: resolvedEditor.editor.editorId ?? '' - }); - - // Make sure it becomes active too - await resolvedEditor.group.openEditor(resolvedEditor.editor); + // Replace editor with resolved one and make active + for (const [group, replacements] of editorReplacements) { + await group.replaceEditors(replacements); + await group.openEditor(replacements[0].replacement); + } } }); - CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); + CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, ...args: unknown[]) => { + const editorGroupsService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group) { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); + if (resolvedContext.groupedEditors.length) { + const { group } = resolvedContext.groupedEditors[0]; await group.closeAllEditors(); - if (group.count === 0 && editorGroupService.getGroup(group.id) /* could be gone by now */) { - editorGroupService.removeGroup(group); // only remove group if it is now empty + if (group.count === 0 && editorGroupsService.getGroup(group.id) /* could be gone by now */) { + editorGroupsService.removeGroup(group); // only remove group if it is now empty } } }); @@ -972,9 +947,9 @@ function registerFocusEditorGroupWihoutWrapCommands(): void { for (const command of commands) { CommandsRegistry.registerCommand(command.id, async (accessor: ServicesAccessor) => { - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); - const group = editorGroupService.findGroup({ direction: command.direction }, editorGroupService.activeGroup, false); + const group = editorGroupsService.findGroup({ direction: command.direction }, editorGroupsService.activeGroup, false); group?.focus(); }); } @@ -982,11 +957,15 @@ function registerFocusEditorGroupWihoutWrapCommands(): void { function registerSplitEditorInGroupCommands(): void { - async function splitEditorInGroup(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + async function splitEditorInGroup(accessor: ServicesAccessor, resolvedContext: IResolvedEditorCommandsContext): Promise { const instantiationService = accessor.get(IInstantiationService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + if (!resolvedContext.groupedEditors.length) { + return; + } + + const { group, editors } = resolvedContext.groupedEditors[0]; + const editor = editors[0]; if (!editor) { return; } @@ -1013,15 +992,22 @@ function registerSplitEditorInGroupCommands(): void { } }); } - run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - return splitEditorInGroup(accessor, resourceOrContext, context); + run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + return splitEditorInGroup(accessor, resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService))); } }); - async function joinEditorInGroup(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + async function joinEditorInGroup(resolvedContext: IResolvedEditorCommandsContext): Promise { + if (!resolvedContext.groupedEditors.length) { + return; + } + + const { group, editors } = resolvedContext.groupedEditors[0]; + const editor = editors[0]; + if (!editor) { + return; + } - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); if (!(editor instanceof SideBySideEditorInput)) { return; } @@ -1059,8 +1045,8 @@ function registerSplitEditorInGroupCommands(): void { } }); } - run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - return joinEditorInGroup(accessor, resourceOrContext, context); + run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + return joinEditorInGroup(resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService))); } }); @@ -1074,14 +1060,18 @@ function registerSplitEditorInGroupCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + if (!resolvedContext.groupedEditors.length) { + return; + } - const { editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (editor instanceof SideBySideEditorInput) { - await joinEditorInGroup(accessor, resourceOrContext, context); - } else if (editor) { - await splitEditorInGroup(accessor, resourceOrContext, context); + const { editors } = resolvedContext.groupedEditors[0]; + + if (editors[0] instanceof SideBySideEditorInput) { + await joinEditorInGroup(resolvedContext); + } else if (editors[0]) { + await splitEditorInGroup(accessor, resolvedContext); } } }); @@ -1195,12 +1185,12 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.Enter), - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - return group.pinEditor(editor); + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + group.pinEditor(editor); + } } } }); @@ -1216,10 +1206,9 @@ function registerOtherEditorCommands(): void { } }); - function setEditorGroupLock(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext, locked?: boolean): void { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + function setEditorGroupLock(accessor: ServicesAccessor, locked: boolean | undefined, ...args: unknown[]): void { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + const group = resolvedContext.groupedEditors[0]?.group; group?.lock(locked ?? !group.isLocked); } @@ -1232,8 +1221,8 @@ function registerOtherEditorCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - setEditorGroupLock(accessor, resourceOrContext, context); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + setEditorGroupLock(accessor, undefined, ...args); } }); @@ -1247,8 +1236,8 @@ function registerOtherEditorCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - setEditorGroupLock(accessor, resourceOrContext, context, true); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + setEditorGroupLock(accessor, true, ...args); } }); @@ -1262,8 +1251,8 @@ function registerOtherEditorCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - setEditorGroupLock(accessor, resourceOrContext, context, false); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + setEditorGroupLock(accessor, false, ...args); } }); @@ -1272,12 +1261,12 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: ActiveEditorStickyContext.toNegated(), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - return group.stickEditor(editor); + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + group.stickEditor(editor); + } } } }); @@ -1289,7 +1278,7 @@ function registerOtherEditorCommands(): void { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.KeyO), handler: async accessor => { const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroupsService = accessor.get(IEditorGroupsService); const activeEditor = editorService.activeEditor; const activeTextEditorControl = editorService.activeTextEditorControl; @@ -1305,7 +1294,7 @@ function registerOtherEditorCommands(): void { editor = activeEditor.modified; } - return editorGroupService.activeGroup.openEditor(editor); + return editorGroupsService.activeGroup.openEditor(editor); } }); @@ -1314,12 +1303,12 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: ActiveEditorStickyContext, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - return group.unstickEditor(editor); + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + group.unstickEditor(editor); + } } } }); @@ -1329,16 +1318,14 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); + handler: (accessor, ...args: unknown[]) => { + const editorGroupsService = accessor.get(IEditorGroupsService); const quickInputService = accessor.get(IQuickInputService); - const commandsContext = getCommandsContext(resourceOrContext, context); - if (commandsContext && typeof commandsContext.groupId === 'number') { - const group = editorGroupService.getGroup(commandsContext.groupId); - if (group) { - editorGroupService.activateGroup(group); // we need the group to be active - } + const commandsContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService)); + const group = commandsContext.groupedEditors[0]?.group; + if (group) { + editorGroupsService.activateGroup(group); // we need the group to be active } return quickInputService.quickAccess.show(ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX); @@ -1346,97 +1333,6 @@ function registerOtherEditorCommands(): void { }); } -function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): { editors: IEditorCommandsContext[]; groups: Array } { - const editorGroupService = accessor.get(IEditorGroupsService); - const listService = accessor.get(IListService); - - const editorContext = getMultiSelectedEditorContexts(getCommandsContext(resourceOrContext, context), listService, editorGroupService); - - const activeGroup = editorGroupService.activeGroup; - if (editorContext.length === 0 && activeGroup.activeEditor) { - // add the active editor as fallback - editorContext.push({ - groupId: activeGroup.id, - editorIndex: activeGroup.getIndexOfEditor(activeGroup.activeEditor) - }); - } - - return { - editors: editorContext, - groups: distinct(editorContext.map(context => context.groupId)).map(groupId => editorGroupService.getGroup(groupId)) - }; -} - -export function getCommandsContext(resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { - if (URI.isUri(resourceOrContext)) { - return context; - } - - if (resourceOrContext && typeof resourceOrContext.groupId === 'number') { - return resourceOrContext; - } - - if (context && typeof context.groupId === 'number') { - return context; - } - - return undefined; -} - -export function resolveCommandsContext(editorGroupService: IEditorGroupsService, context?: IEditorCommandsContext): { group: IEditorGroup; editor?: EditorInput } { - - // Resolve from context - let group = context && typeof context.groupId === 'number' ? editorGroupService.getGroup(context.groupId) : undefined; - let editor = group && context && typeof context.editorIndex === 'number' ? group.getEditorByIndex(context.editorIndex) ?? undefined : undefined; - - // Fallback to active group as needed - if (!group) { - group = editorGroupService.activeGroup; - } - - // Fallback to active editor as needed - if (!editor) { - editor = group.activeEditor ?? undefined; - } - - return { group, editor }; -} - -export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsContext | undefined, listService: IListService, editorGroupService: IEditorGroupsService): IEditorCommandsContext[] { - - // First check for a focused list to return the selected items from - const list = listService.lastFocusedList; - if (list instanceof List && list.getHTMLElement() === getActiveElement()) { - const elementToContext = (element: IEditorIdentifier | IEditorGroup) => { - if (isEditorGroup(element)) { - return { groupId: element.id, editorIndex: undefined }; - } - - const group = editorGroupService.getGroup(element.groupId); - - return { groupId: element.groupId, editorIndex: group ? group.getIndexOfEditor(element.editor) : -1 }; - }; - - const onlyEditorGroupAndEditor = (e: IEditorIdentifier | IEditorGroup) => isEditorGroup(e) || isEditorIdentifier(e); - - const focusedElements: Array = list.getFocusedElements().filter(onlyEditorGroupAndEditor); - const focus = editorContext ? editorContext : focusedElements.length ? focusedElements.map(elementToContext)[0] : undefined; // need to take into account when editor context is { group: group } - - if (focus) { - const selection: Array = list.getSelectedElements().filter(onlyEditorGroupAndEditor); - - if (selection.length > 1) { - return selection.map(elementToContext); - } - - return [focus]; - } - } - - // Otherwise go with passed in context - return !!editorContext ? [editorContext] : []; -} - export function setup(): void { registerActiveEditorMoveCopyCommand(); registerEditorGroupsLayoutCommands(); diff --git a/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts b/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts new file mode 100644 index 00000000000..b9fea217efb --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts @@ -0,0 +1,200 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveElement } from 'vs/base/browser/dom'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { URI } from 'vs/base/common/uri'; +import { IListService } from 'vs/platform/list/browser/listService'; +import { IEditorCommandsContext, isEditorCommandsContext, IEditorIdentifier, isEditorIdentifier } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorGroup, IEditorGroupsService, isEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export interface IResolvedEditorCommandsContext { + readonly groupedEditors: { + readonly group: IEditorGroup; + readonly editors: EditorInput[]; + }[]; + readonly preserveFocus: boolean; +} + +export function resolveCommandsContext(commandArgs: unknown[], editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IResolvedEditorCommandsContext { + + const commandContext = getCommandsContext(commandArgs, editorService, editorGroupsService, listService); + const preserveFocus = commandContext.length ? commandContext[0].preserveFocus || false : false; + const resolvedContext: IResolvedEditorCommandsContext = { groupedEditors: [], preserveFocus }; + + for (const editorContext of commandContext) { + const groupAndEditor = getEditorAndGroupFromContext(editorContext, editorGroupsService); + if (!groupAndEditor) { + continue; + } + + const { group, editor } = groupAndEditor; + + // Find group context if already added + let groupContext = undefined; + for (const targetGroupContext of resolvedContext.groupedEditors) { + if (targetGroupContext.group.id === group.id) { + groupContext = targetGroupContext; + break; + } + } + + // Otherwise add new group context + if (!groupContext) { + groupContext = { group, editors: [] }; + resolvedContext.groupedEditors.push(groupContext); + } + + // Add editor to group context + if (editor) { + groupContext.editors.push(editor); + } + } + + return resolvedContext; +} + +function getCommandsContext(commandArgs: unknown[], editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IEditorCommandsContext[] { + // Figure out if command is executed from a list + const list = listService.lastFocusedList; + let isListAction = list instanceof List && list.getHTMLElement() === getActiveElement(); + + // Get editor context for which the command was triggered + let editorContext = getEditorContextFromCommandArgs(commandArgs, isListAction, editorService, editorGroupsService, listService); + + // If the editor context can not be determind use the active editor + if (!editorContext) { + const activeGroup = editorGroupsService.activeGroup; + const activeEditor = activeGroup.activeEditor; + editorContext = { groupId: activeGroup.id, editorIndex: activeEditor ? activeGroup.getIndexOfEditor(activeEditor) : undefined }; + isListAction = false; + } + + const multiEditorContext = getMultiSelectContext(editorContext, isListAction, editorService, editorGroupsService, listService); + + // Make sure the command context is the first one in the list + return moveCurrentEditorContextToFront(editorContext, multiEditorContext); +} + +function moveCurrentEditorContextToFront(editorContext: IEditorCommandsContext, multiEditorContext: IEditorCommandsContext[]): IEditorCommandsContext[] { + if (multiEditorContext.length <= 1) { + return multiEditorContext; + } + + const editorContextIndex = multiEditorContext.findIndex(context => + context.groupId === editorContext.groupId && + context.editorIndex === editorContext.editorIndex + ); + + if (editorContextIndex !== -1) { + multiEditorContext.splice(editorContextIndex, 1); + multiEditorContext.unshift(editorContext); + } else if (editorContext.editorIndex === undefined) { + multiEditorContext.unshift(editorContext); + } else { + throw new Error('Editor context not found in multi editor context'); + } + + return multiEditorContext; +} + +function getEditorContextFromCommandArgs(commandArgs: unknown[], isListAction: boolean, editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IEditorCommandsContext | undefined { + // We only know how to extraxt the command context from URI and IEditorCommandsContext arguments + const filteredArgs = commandArgs.filter(arg => isEditorCommandsContext(arg) || URI.isUri(arg)); + + // If the command arguments contain an editor context, use it + for (const arg of filteredArgs) { + if (isEditorCommandsContext(arg)) { + return arg; + } + } + + // Otherwise, try to find the editor group by the URI of the resource + for (const uri of filteredArgs as URI[]) { + const editorIdentifiers = editorService.findEditors(uri); + if (editorIdentifiers.length) { + const editorIdentifier = editorIdentifiers[0]; + const group = editorGroupsService.getGroup(editorIdentifier.groupId); + return { groupId: editorIdentifier.groupId, editorIndex: group?.getIndexOfEditor(editorIdentifier.editor) }; + } + } + + // If there is no context in the arguments, try to find the context from the focused list + // if the action was executed from a list + if (isListAction) { + const list = listService.lastFocusedList as List; + for (const focusedElement of list.getFocusedElements()) { + if (isGroupOrEditor(focusedElement)) { + return groupOrEditorToEditorContext(focusedElement, undefined, editorGroupsService); + } + } + } + + return undefined; +} + +function getMultiSelectContext(editorContext: IEditorCommandsContext, isListAction: boolean, editorService: IEditorService, editorGroupsService: IEditorGroupsService, listService: IListService): IEditorCommandsContext[] { + + // If the action was executed from a list, return all selected editors + if (isListAction) { + const list = listService.lastFocusedList as List; + const selection = list.getSelectedElements().filter(isGroupOrEditor); + + if (selection.length > 1) { + return selection.map(e => groupOrEditorToEditorContext(e, editorContext.preserveFocus, editorGroupsService)); + } + + if (selection.length === 0) { + // TODO@benibenj workaround for https://github.com/microsoft/vscode/issues/224050 + // Explainer: the `isListAction` flag can be a false positive in certain cases because + // it will be `true` if the active element is a `List` even if it is part of the editor + // area. The workaround here is to fallback to `isListAction: false` if the list is not + // having any editor or group selected. + return getMultiSelectContext(editorContext, false, editorService, editorGroupsService, listService); + } + } + // Check editors selected in the group (tabs) + else { + const group = editorGroupsService.getGroup(editorContext.groupId); + const editor = editorContext.editorIndex !== undefined ? group?.getEditorByIndex(editorContext.editorIndex) : group?.activeEditor; + // If the editor is selected, return all selected editors otherwise only use the editors context + if (group && editor && group.isSelected(editor)) { + return group.selectedEditors.map(editor => groupOrEditorToEditorContext({ editor, groupId: group.id }, editorContext.preserveFocus, editorGroupsService)); + } + } + + // Otherwise go with passed in context + return [editorContext]; +} + +function groupOrEditorToEditorContext(element: IEditorIdentifier | IEditorGroup, preserveFocus: boolean | undefined, editorGroupsService: IEditorGroupsService): IEditorCommandsContext { + if (isEditorGroup(element)) { + return { groupId: element.id, editorIndex: undefined, preserveFocus }; + } + + const group = editorGroupsService.getGroup(element.groupId); + + return { groupId: element.groupId, editorIndex: group ? group.getIndexOfEditor(element.editor) : -1, preserveFocus }; +} + +function isGroupOrEditor(element: unknown): element is IEditorIdentifier | IEditorGroup { + return isEditorGroup(element) || isEditorIdentifier(element); +} + +function getEditorAndGroupFromContext(commandContext: IEditorCommandsContext, editorGroupsService: IEditorGroupsService): { group: IEditorGroup; editor: EditorInput | undefined } | undefined { + const group = editorGroupsService.getGroup(commandContext.groupId); + if (!group) { + return undefined; + } + + if (commandContext.editorIndex === undefined) { + return { group, editor: undefined }; + } + + const editor = group.getEditorByIndex(commandContext.editorIndex); + return { group, editor }; +} diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 14f3ad5b728..b11c66e6057 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -92,7 +92,7 @@ class DropOverlay extends Themable { this.groupView.element.appendChild(container); this.groupView.element.classList.add('dragged-over'); this._register(toDisposable(() => { - this.groupView.element.removeChild(container); + container.remove(); this.groupView.element.classList.remove('dragged-over'); })); @@ -165,7 +165,7 @@ class DropOverlay extends Themable { isCopy = this.isCopyOperation(e); } else if (isDraggingEditor) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { isCopy = this.isCopyOperation(e, data[0].identifier); } } @@ -234,7 +234,7 @@ class DropOverlay extends Themable { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { return this.editorGroupService.getGroup(data[0].identifier); } } @@ -242,7 +242,7 @@ class DropOverlay extends Themable { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { return this.editorGroupService.getGroup(data[0].identifier.groupId); } } @@ -267,7 +267,7 @@ class DropOverlay extends Themable { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const sourceGroup = this.editorGroupService.getGroup(data[0].identifier); if (sourceGroup) { if (typeof splitDirection !== 'number' && sourceGroup === this.groupView) { @@ -306,12 +306,13 @@ class DropOverlay extends Themable { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { - const draggedEditor = data[0].identifier; + if (Array.isArray(data) && data.length > 0) { + const draggedEditors = data; + const firstDraggedEditor = data[0].identifier; - const sourceGroup = this.editorGroupService.getGroup(draggedEditor.groupId); + const sourceGroup = this.editorGroupService.getGroup(firstDraggedEditor.groupId); if (sourceGroup) { - const copyEditor = this.isCopyOperation(event, draggedEditor); + const copyEditor = this.isCopyOperation(event, firstDraggedEditor); let targetGroup: IEditorGroup | undefined = undefined; // Optimization: if we move the last editor of an editor group @@ -328,16 +329,20 @@ class DropOverlay extends Themable { return; } - // Open in target group - const options = fillActiveEditorViewState(sourceGroup, draggedEditor.editor, { - pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state - }); + const editors = draggedEditors.map(draggedEditor => ( + { + editor: draggedEditor.identifier.editor, + options: fillActiveEditorViewState(sourceGroup, draggedEditor.identifier.editor, { + pinned: true, // always pin dropped editor + sticky: sourceGroup.isSticky(draggedEditor.identifier.editor) // preserve sticky state + }) + } + )); if (!copyEditor) { - sourceGroup.moveEditor(draggedEditor.editor, targetGroup, options); + sourceGroup.moveEditors(editors, targetGroup); } else { - sourceGroup.copyEditor(draggedEditor.editor, targetGroup, options); + sourceGroup.copyEditors(editors, targetGroup); } } @@ -352,7 +357,7 @@ class DropOverlay extends Themable { // Check for tree items else if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) { const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const editors: IUntypedEditorInput[] = []; for (const id of data) { const dataTransferItem = await this.treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 16b04556d21..41e4e3ccc24 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -5,8 +5,8 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroupModel, IEditorOpenOptions, IGroupModelChangeEvent, ISerializedEditorGroupModel, isGroupEditorCloseEvent, isGroupEditorOpenEvent, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; -import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorResourceAccessor, EditorInputCapabilities, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, SideBySideEditor, EditorCloseContext, IEditorWillMoveEvent, IEditorWillOpenEvent, IMatchEditorOptions, GroupModelChangeKind, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions } from 'vs/workbench/common/editor'; -import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext } from 'vs/workbench/common/contextkeys'; +import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorResourceAccessor, EditorInputCapabilities, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, SideBySideEditor, EditorCloseContext, IEditorWillMoveEvent, IEditorWillOpenEvent, IMatchEditorOptions, GroupModelChangeKind, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions, TEXT_DIFF_EDITOR_ID } from 'vs/workbench/common/editor'; +import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorContext, ActiveEditorReadonlyContext, ActiveEditorCanRevertContext, ActiveEditorCanToggleReadonlyContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; @@ -28,7 +28,7 @@ import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions, IEditorPartsView, IEditorGroupViewOptions } from 'vs/workbench/browser/parts/editor/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; @@ -56,21 +56,23 @@ import { EditorTitleControl } from 'vs/workbench/browser/parts/editor/editorTitl import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; export class EditorGroupView extends Themable implements IEditorGroupView { //#region factory - static createNew(editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, null, editorPartsView, groupsView, groupsLabel, groupIndex); + static createNew(editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, null, editorPartsView, groupsView, groupsLabel, groupIndex, options); } - static createFromSerialized(serialized: ISerializedEditorGroupModel, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, serialized, editorPartsView, groupsView, groupsLabel, groupIndex); + static createFromSerialized(serialized: ISerializedEditorGroupModel, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, serialized, editorPartsView, groupsView, groupsLabel, groupIndex, options); } - static createCopy(copyFrom: IEditorGroupView, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, copyFrom, editorPartsView, groupsView, groupsLabel, groupIndex); + static createCopy(copyFrom: IEditorGroupView, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, copyFrom, editorPartsView, groupsView, groupsLabel, groupIndex, options); } //#endregion @@ -143,6 +145,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { readonly groupsView: IEditorGroupsView, private groupsLabel: string, private _index: number, + options: IEditorGroupViewOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @@ -157,7 +160,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @ILogService private readonly logService: ILogService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IHostService private readonly hostService: IHostService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IFileService private readonly fileService: IFileService ) { super(themeService); @@ -194,10 +198,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.progressBar.hide(); // Scoped instantiation service - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, this.scopedContextKeyService], [IEditorProgressService, this._register(new EditorProgressIndicator(this.progressBar, this))] - )); + ))); // Context keys this.resourceContext = this._register(this.scopedInstantiationService.createInstance(ResourceContextKey)); @@ -233,7 +237,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#endregion // Restore editors if provided - const restoreEditorsPromise = this.restoreEditors(from) ?? Promise.resolve(); + const restoreEditorsPromise = this.restoreEditors(from, options) ?? Promise.resolve(); // Signal restored once editors have restored restoreEditorsPromise.finally(() => { @@ -245,17 +249,29 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private handleGroupContextKeys(): void { - const groupActiveEditorDirtyContext = ActiveEditorDirtyContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorPinnedContext = ActiveEditorPinnedContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorFirstContext = ActiveEditorFirstInGroupContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorLastContext = ActiveEditorLastInGroupContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorStickyContext = ActiveEditorStickyContext.bindTo(this.scopedContextKeyService); - const groupEditorsCountContext = EditorGroupEditorsCountContext.bindTo(this.scopedContextKeyService); - const groupLockedContext = ActiveEditorGroupLockedContext.bindTo(this.scopedContextKeyService); + const groupActiveEditorDirtyContext = this.editorPartsView.bind(ActiveEditorDirtyContext, this); + const groupActiveEditorPinnedContext = this.editorPartsView.bind(ActiveEditorPinnedContext, this); + const groupActiveEditorFirstContext = this.editorPartsView.bind(ActiveEditorFirstInGroupContext, this); + const groupActiveEditorLastContext = this.editorPartsView.bind(ActiveEditorLastInGroupContext, this); + const groupActiveEditorStickyContext = this.editorPartsView.bind(ActiveEditorStickyContext, this); + const groupEditorsCountContext = this.editorPartsView.bind(EditorGroupEditorsCountContext, this); + const groupLockedContext = this.editorPartsView.bind(ActiveEditorGroupLockedContext, this); - const groupActiveEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorCanSplitInGroupContext = ActiveEditorCanSplitInGroupContext.bindTo(this.scopedContextKeyService); - const sideBySideEditorContext = SideBySideEditorActiveContext.bindTo(this.scopedContextKeyService); + const multipleEditorsSelectedContext = MultipleEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); + const twoEditorsSelectedContext = TwoEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); + const selectedEditorsHaveFileOrUntitledResourceContext = SelectedEditorsInGroupFileOrUntitledResourceContextKey.bindTo(this.scopedContextKeyService); + + const groupActiveEditorContext = this.editorPartsView.bind(ActiveEditorContext, this); + const groupActiveEditorIsReadonly = this.editorPartsView.bind(ActiveEditorReadonlyContext, this); + const groupActiveEditorCanRevert = this.editorPartsView.bind(ActiveEditorCanRevertContext, this); + const groupActiveEditorCanToggleReadonly = this.editorPartsView.bind(ActiveEditorCanToggleReadonlyContext, this); + const groupActiveCompareEditorCanSwap = this.editorPartsView.bind(ActiveCompareEditorCanSwapContext, this); + const groupTextCompareEditorVisibleContext = this.editorPartsView.bind(TextCompareEditorVisibleContext, this); + const groupTextCompareEditorActiveContext = this.editorPartsView.bind(TextCompareEditorActiveContext, this); + + const groupActiveEditorAvailableEditorIds = this.editorPartsView.bind(ActiveEditorAvailableEditorIdsContext, this); + const groupActiveEditorCanSplitInGroupContext = this.editorPartsView.bind(ActiveEditorCanSplitInGroupContext, this); + const groupActiveEditorIsSideBySideEditorContext = this.editorPartsView.bind(SideBySideEditorActiveContext, this); const activeEditorListener = this._register(new MutableDisposable()); @@ -264,22 +280,46 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.scopedContextKeyService.bufferChangeEvents(() => { const activeEditor = this.activeEditor; + const activeEditorPane = this.activeEditorPane; this.resourceContext.set(EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY })); applyAvailableEditorIds(groupActiveEditorAvailableEditorIds, activeEditor, this.editorResolverService); - groupActiveEditorCanSplitInGroupContext.set(activeEditor ? activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup) : false); - sideBySideEditorContext.set(activeEditor?.typeId === SideBySideEditorInput.ID); - if (activeEditor) { + groupActiveEditorCanSplitInGroupContext.set(activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup)); + groupActiveEditorIsSideBySideEditorContext.set(activeEditor.typeId === SideBySideEditorInput.ID); + groupActiveEditorDirtyContext.set(activeEditor.isDirty() && !activeEditor.isSaving()); activeEditorListener.value = activeEditor.onDidChangeDirty(() => { groupActiveEditorDirtyContext.set(activeEditor.isDirty() && !activeEditor.isSaving()); }); } else { + groupActiveEditorCanSplitInGroupContext.set(false); + groupActiveEditorIsSideBySideEditorContext.set(false); groupActiveEditorDirtyContext.set(false); } + + if (activeEditorPane) { + groupActiveEditorContext.set(activeEditorPane.getId()); + groupActiveEditorCanRevert.set(!activeEditorPane.input.hasCapability(EditorInputCapabilities.Untitled)); + groupActiveEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); + + const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); + const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); + groupActiveCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); + groupActiveEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); + + const activePaneDiffEditor = activeEditorPane?.getId() === TEXT_DIFF_EDITOR_ID; + groupTextCompareEditorActiveContext.set(activePaneDiffEditor); + groupTextCompareEditorVisibleContext.set(activePaneDiffEditor); + } else { + groupActiveEditorContext.reset(); + groupActiveEditorCanRevert.reset(); + groupActiveEditorIsReadonly.reset(); + groupActiveCompareEditorCanSwap.reset(); + groupActiveEditorCanToggleReadonly.reset(); + } }); }; @@ -296,6 +336,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { groupActiveEditorStickyContext.set(this.model.activeEditor ? this.model.isSticky(this.model.activeEditor) : false); break; case GroupModelChangeKind.EDITOR_CLOSE: + groupActiveEditorPinnedContext.set(this.model.activeEditor ? this.model.isPinned(this.model.activeEditor) : false); + groupActiveEditorStickyContext.set(this.model.activeEditor ? this.model.isSticky(this.model.activeEditor) : false); case GroupModelChangeKind.EDITOR_OPEN: case GroupModelChangeKind.EDITOR_MOVE: groupActiveEditorFirstContext.set(this.model.isFirst(this.model.activeEditor)); @@ -311,6 +353,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { groupActiveEditorStickyContext.set(this.model.isSticky(this.model.activeEditor)); } break; + case GroupModelChangeKind.EDITORS_SELECTION: + multipleEditorsSelectedContext.set(this.model.selectedEditors.length > 1); + twoEditorsSelectedContext.set(this.model.selectedEditors.length === 2); + selectedEditorsHaveFileOrUntitledResourceContext.set(this.model.selectedEditors.every(e => e.resource && (this.fileService.hasProvider(e.resource) || e.resource.scheme === Schemas.untitled))); + break; } // Group editors count context @@ -490,7 +537,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleContainer.classList.toggle('show-file-icons', this.groupsView.partOptions.showIcons); } - private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null): Promise | undefined { + private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null, groupViewOptions?: IEditorGroupViewOptions): Promise | undefined { if (this.count === 0) { return; // nothing to show } @@ -513,24 +560,30 @@ export class EditorGroupView extends Themable implements IEditorGroupView { options.preserveFocus = true; // handle focus after editor is restored const internalOptions: IInternalEditorOpenOptions = { - preserveWindowOrder: true // handle window order after editor is restored + preserveWindowOrder: true, // handle window order after editor is restored + skipTitleUpdate: true, // update the title later for all editors at once }; const activeElement = getActiveElement(); // Show active editor (intentionally not using async to keep // `restoreEditors` from executing in same stack) - return this.doShowEditor(activeEditor, { active: true, isNew: false /* restored */ }, options, internalOptions).then(() => { + const result = this.doShowEditor(activeEditor, { active: true, isNew: false /* restored */ }, options, internalOptions).then(() => { // Set focused now if this is the active group and focus has // not changed meanwhile. This prevents focus from being // stolen accidentally on startup when the user already // clicked somewhere. - if (this.groupsView.activeGroup === this && activeElement && isActiveElement(activeElement)) { + if (this.groupsView.activeGroup === this && activeElement && isActiveElement(activeElement) && !groupViewOptions?.preserveFocus) { this.focus(); } }); + + // Restore editors in title control + this.titleControl.openEditors(this.editors); + + return result; } //#region event handling @@ -557,8 +610,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Handle within - if (e.kind === GroupModelChangeKind.GROUP_LOCKED) { - this.element.classList.toggle('locked', this.isLocked); + switch (e.kind) { + case GroupModelChangeKind.GROUP_LOCKED: + this.element.classList.toggle('locked', this.isLocked); + break; + case GroupModelChangeKind.EDITORS_SELECTION: + this.onDidChangeEditorSelection(); + break; } if (!e.editor) { @@ -664,15 +722,23 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (!resource) { return undefined; } + const path = resource ? resource.scheme === Schemas.file ? resource.fsPath : resource.path : undefined; if (!path) { return undefined; } - let resourceExt = extname(resource); + // Remove query parameters from the resource extension + let resourceExt = extname(resource); const queryStringLocation = resourceExt.indexOf('?'); resourceExt = queryStringLocation !== -1 ? resourceExt.substr(0, queryStringLocation) : resourceExt; - return { mimeType: new TelemetryTrustedValue(getMimeTypes(resource).join(', ')), scheme: resource.scheme, ext: resourceExt, path: hash(path) }; + + return { + mimeType: new TelemetryTrustedValue(getMimeTypes(resource).join(', ')), + scheme: resource.scheme, + ext: resourceExt, + path: hash(path) + }; } private toEditorTelemetryDescriptor(editor: EditorInput): object { @@ -769,7 +835,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Ensure to show active editor if any if (this.model.activeEditor) { - this.titleControl.openEditor(this.model.activeEditor); + this.titleControl.openEditors(this.model.getEditors(EditorsOrder.SEQUENTIAL)); } } @@ -810,6 +876,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleControl.updateEditorLabel(editor); } + private onDidChangeEditorSelection(): void { + + // Forward to title control + this.titleControl.updateEditorSelections(); + } + private onDidVisibilityChange(visible: boolean): void { // Forward to active editor pane @@ -882,6 +954,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { setActive(isActive: boolean): void { this.active = isActive; + // Clear selection when group no longer active + if (!isActive && this.activeEditor && this.selectedEditors.length > 1) { + this.setSelection(this.activeEditor, []); + } + // Update container this.element.classList.toggle('active', isActive); this.element.classList.toggle('inactive', !isActive); @@ -928,6 +1005,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.activeEditor; } + get selectedEditors(): EditorInput[] { + return this.model.selectedEditors; + } + get previewEditor(): EditorInput | null { return this.model.previewEditor; } @@ -940,6 +1021,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isSticky(editorOrIndex); } + isSelected(editor: EditorInput): boolean { + return this.model.isSelected(editor); + } + isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } @@ -948,6 +1033,17 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isActive(editor); } + async setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): Promise { + if (!this.isActive(activeSelectedEditor)) { + // The active selected editor is not yet opened, so we go + // through `openEditor` to show it. We pass the inactive + // selection as internal options + await this.openEditor(activeSelectedEditor, { activation: EditorActivation.ACTIVATE }, { inactiveSelection: inactiveSelectedEditors }); + } else { + this.model.setSelection(activeSelectedEditor, inactiveSelectedEditors); + } + } + contains(candidate: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean { return this.model.contains(candidate, options); } @@ -1098,6 +1194,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { pinned, sticky: options?.sticky || (typeof options?.index === 'number' && this.model.isSticky(options.index)), transient: !!options?.transient, + inactiveSelection: internalOptions?.inactiveSelection, active: this.count === 0 || !options || !options.inactive, supportSideBySide: internalOptions?.supportSideBySide }; @@ -1859,8 +1956,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return true; } + // Apply the `excludeConfirming` filter if present + let editors = this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options); + if (options?.excludeConfirming) { + editors = editors.filter(editor => !this.shouldConfirmClose(editor)); + } + // Check for confirmation and veto - const veto = await this.handleCloseConfirmation(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options)); + const veto = await this.handleCloseConfirmation(editors); if (veto) { return false; } @@ -1872,10 +1975,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private doCloseAllEditors(options?: ICloseAllEditorsOptions): void { + let editors = this.model.getEditors(EditorsOrder.SEQUENTIAL, options); + if (options?.excludeConfirming) { + editors = editors.filter(editor => !this.shouldConfirmClose(editor)); + } // Close all inactive editors first const editorsToClose: EditorInput[] = []; - for (const editor of this.model.getEditors(EditorsOrder.SEQUENTIAL, options)) { + for (const editor of editors) { if (!this.isActive(editor)) { this.doCloseInactiveEditor(editor); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 77d8a445857..c9ac110bc23 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -62,7 +62,7 @@ export class EditorGroupWatermark extends Disposable { private readonly transientDisposables = this._register(new DisposableStore()); private enabled: boolean = false; private workbenchState: WorkbenchState; - private keybindingLabel?: KeybindingLabel; + private keybindingLabels = new Set(); constructor( container: HTMLElement, @@ -137,6 +137,9 @@ export class EditorGroupWatermark extends Disposable { const update = () => { clearNode(box); + this.keybindingLabels.forEach(label => label.dispose()); + this.keybindingLabels.clear(); + for (const entry of selected) { const keys = this.keybindingService.lookupKeybinding(entry.id); if (!keys) { @@ -146,9 +149,9 @@ export class EditorGroupWatermark extends Disposable { const dt = append(dl, $('dt')); dt.textContent = entry.text; const dd = append(dl, $('dd')); - this.keybindingLabel?.dispose(); - this.keybindingLabel = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); - this.keybindingLabel.set(keys); + const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); + label.set(keys); + this.keybindingLabels.add(label); } }; @@ -164,6 +167,6 @@ export class EditorGroupWatermark extends Disposable { override dispose(): void { super.dispose(); this.clear(); - this.keybindingLabel?.dispose(); + this.keybindingLabels.forEach(label => label.dispose()); } } diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 5d638871273..ef1b25dd681 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -466,7 +466,7 @@ export class EditorPanes extends Disposable { // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); if (editorPaneContainer) { - this.editorPanesParent.removeChild(editorPaneContainer); + editorPaneContainer.remove(); hide(editorPaneContainer); } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 8b37f345733..c305eb57158 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -5,8 +5,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Part } from 'vs/workbench/browser/part'; -import { Dimension, $, EventHelper, addDisposableGenericMouseDownListener, getWindow, isAncestorOfActiveElement, getActiveElement } from 'vs/base/browser/dom'; -import { Event, Emitter, Relay } from 'vs/base/common/event'; +import { Dimension, $, EventHelper, addDisposableGenericMouseDownListener, getWindow, isAncestorOfActiveElement, getActiveElement, isHTMLElement } from 'vs/base/browser/dom'; +import { Event, Emitter, Relay, PauseableEmitter } from 'vs/base/common/event'; import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorSideGroup, IEditorDropTargetDelegate, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -14,7 +14,7 @@ import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGr import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce } from 'vs/base/common/arrays'; -import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView, IEditorGroupsView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView, IEditorGroupsView, IEditorGroupViewOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -114,10 +114,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { private readonly _onDidActivateGroup = this._register(new Emitter()); readonly onDidActivateGroup = this._onDidActivateGroup.event; - private readonly _onDidAddGroup = this._register(new Emitter()); + private readonly _onDidAddGroup = this._register(new PauseableEmitter()); readonly onDidAddGroup = this._onDidAddGroup.event; - private readonly _onDidRemoveGroup = this._register(new Emitter()); + private readonly _onDidRemoveGroup = this._register(new PauseableEmitter()); readonly onDidRemoveGroup = this._onDidRemoveGroup.event; private readonly _onDidMoveGroup = this._register(new Emitter()); @@ -134,6 +134,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { private readonly _onDidChangeEditorPartOptions = this._register(new Emitter()); readonly onDidChangeEditorPartOptions = this._onDidChangeEditorPartOptions.event; + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + //#endregion private readonly workspaceMemento = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER); @@ -615,16 +618,16 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { } } - private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroupModel | null): IEditorGroupView { + private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroupModel | null, options?: IEditorGroupViewOptions): IEditorGroupView { // Create group view let groupView: IEditorGroupView; if (from instanceof EditorGroupView) { - groupView = EditorGroupView.createCopy(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService,); + groupView = EditorGroupView.createCopy(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } else if (isSerializedEditorGroupModel(from)) { - groupView = EditorGroupView.createFromSerialized(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService); + groupView = EditorGroupView.createFromSerialized(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } else { - groupView = EditorGroupView.createNew(this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService); + groupView = EditorGroupView.createNew(this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } // Keep in map @@ -867,7 +870,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { return copiedGroupView; } - mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroupView { + mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): boolean { const sourceView = this.assertGroupView(group); const targetView = this.assertGroupView(target); @@ -885,10 +888,11 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { } // Move/Copy editors over into target + let result = true; if (options?.mode === MergeGroupMode.COPY_EDITORS) { sourceView.copyEditors(editors, targetView); } else { - sourceView.moveEditors(editors, targetView); + result = sourceView.moveEditors(editors, targetView); } // Remove source if the view is now empty and not already removed @@ -896,21 +900,25 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { this.removeGroup(sourceView, true); } - return targetView; + return result; } - mergeAllGroups(target: IEditorGroupView | GroupIdentifier): IEditorGroupView { + mergeAllGroups(target: IEditorGroupView | GroupIdentifier): boolean { const targetView = this.assertGroupView(target); + let result = true; for (const group of this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { if (group === targetView) { continue; // keep target } - this.mergeGroup(group, targetView); + const merged = this.mergeGroup(group, targetView); + if (!merged) { + result = false; + } } - return targetView; + return result; } protected assertGroupView(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { @@ -929,7 +937,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { } createEditorDropTarget(container: unknown, delegate: IEditorDropTargetDelegate): IDisposable { - assertType(container instanceof HTMLElement); + assertType(isHTMLElement(container)); return this.scopedInstantiationService.createInstance(EditorDropTarget, container, delegate); } @@ -973,9 +981,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { // Scoped instantiation service const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.container)); - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, scopedContextKeyService] - )); + ))); // Grid control this._willRestoreState = !options || options.restorePreviousState; @@ -1098,6 +1106,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { openVerticalPosition = Position.BOTTOM; } + if (e.eventData.clientY < boundingRect.top + proximity) { + openVerticalPosition = Position.TOP; + } + if (horizontalOpenerTimeout && openHorizontalPosition !== lastOpenHorizontalPosition) { clearTimeout(horizontalOpenerTimeout); horizontalOpenerTimeout = undefined; @@ -1187,7 +1199,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { return true; // success } - private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[], options?: IEditorGroupViewOptions): void { // Determine group views to reuse if any let reuseGroupViews: IEditorGroupView[]; @@ -1205,7 +1217,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { if (reuseGroupViews.length > 0) { groupView = reuseGroupViews.shift()!; } else { - groupView = this.doCreateGroupView(serializedEditorGroup); + groupView = this.doCreateGroupView(serializedEditorGroup, options); } groupViews.push(groupView); @@ -1337,30 +1349,79 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { }; } - async applyState(state: IEditorPartUIState): Promise { - - // Close all opened editors and dispose groups - for (const group of this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { - const closed = await group.closeAllEditors(); - if (!closed) { - return false; - } + applyState(state: IEditorPartUIState | 'empty', options?: IEditorGroupViewOptions): Promise { + if (state === 'empty') { + return this.doApplyEmptyState(); + } else { + return this.doApplyState(state, options); } + } + + private async doApplyState(state: IEditorPartUIState, options?: IEditorGroupViewOptions): Promise { + const groups = await this.doPrepareApplyState(); + + // Pause add/remove events for groups during the duration of applying the state + // This ensures that we can do this transition atomically with the new state + // being ready when the events are fired. This is important because usually there + // is never the state where no groups are present, but for this transition we + // need to temporarily dispose all groups to restore the new set. + + this._onDidAddGroup.pause(); + this._onDidRemoveGroup.pause(); + this.disposeGroups(); // MRU this.mostRecentActiveGroups = state.mostRecentActiveGroups; // Grid Widget - this.doApplyGridState(state.serializedGrid, state.activeGroup); + try { + this.doApplyGridState(state.serializedGrid, state.activeGroup, undefined, options); + } finally { + // It is very important to keep this order: first resume the events for + // removed groups and then for added groups. Many listeners may store + // groups in sets by their identifier and groups can have the same + // identifier before and after. + this._onDidRemoveGroup.resume(); + this._onDidAddGroup.resume(); + } - return true; + // Restore editors that were not closed before and are now opened now + await this.activeGroup.openEditors( + groups + .flatMap(group => group.editors) + .filter(editor => this.editorPartsView.groups.every(groupView => !groupView.contains(editor))) + .map(editor => ({ + editor, options: { pinned: true, preserveFocus: true, inactive: true } + })) + ); } - private doApplyGridState(gridState: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + private async doApplyEmptyState(): Promise { + await this.doPrepareApplyState(); + + this.mergeAllGroups(this.activeGroup); + } + + private async doPrepareApplyState(): Promise { + + // Before disposing groups, try to close as many editors as + // possible, but skip over those that would trigger a dialog + // (for example when being dirty). This is to be able to later + // restore these editors after state has been applied. + + const groups = this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); + for (const group of groups) { + await group.closeAllEditors({ excludeConfirming: true }); + } + + return groups; + } + + private doApplyGridState(gridState: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[], options?: IEditorGroupViewOptions): void { // Recreate grid widget from state - this.doCreateGridControlWithState(gridState, activeGroupId, editorGroupViewsToReuse); + this.doCreateGridControlWithState(gridState, activeGroupId, editorGroupViewsToReuse, options); // Layout this.doLayout(this._contentDimension); @@ -1409,6 +1470,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { override dispose(): void { + // Event + this._onWillDispose.fire(); + // Forward to all groups this.disposeGroups(); diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 35b9750480d..c04fd7769ac 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Emitter } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { EditorPart, IEditorPartUIState, MainEditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupView, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; @@ -18,19 +18,26 @@ import { MultiWindowParts } from 'vs/workbench/browser/part'; import { DeferredPromise } from 'vs/base/common/async'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IRectangle } from 'vs/platform/window/common/window'; -import { getWindow } from 'vs/base/browser/dom'; -import { getZoomLevel } from 'vs/base/browser/browser'; +import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { isHTMLElement } from 'vs/base/browser/dom'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; interface IEditorPartsUIState { readonly auxiliary: IAuxiliaryEditorPartState[]; readonly mru: number[]; + // main state is managed by the main part } -interface IAuxiliaryEditorPartState { +interface IAuxiliaryEditorPartState extends IAuxiliaryWindowOpenOptions { readonly state: IEditorPartUIState; - readonly bounds?: IRectangle; - readonly zoomLevel?: number; +} + +interface IEditorWorkingSetState extends IEditorWorkingSet { + readonly main: IEditorPartUIState; + readonly auxiliary: IEditorPartsUIState; } export class EditorParts extends MultiWindowParts implements IEditorGroupsService, IEditorPartsView { @@ -42,9 +49,11 @@ export class EditorParts extends MultiWindowParts implements IEditor private mostRecentActiveParts = [this.mainPart]; constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IStorageService storageService: IStorageService, - @IThemeService themeService: IThemeService + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IStorageService private readonly storageService: IStorageService, + @IThemeService themeService: IThemeService, + @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super('workbench.editorParts', themeService, storageService); @@ -56,25 +65,51 @@ export class EditorParts extends MultiWindowParts implements IEditor private registerListeners(): void { this._register(this.onDidChangeMementoValue(StorageScope.WORKSPACE, this._store)(e => this.onDidChangeMementoState(e))); + this.whenReady.then(() => this.registerGroupsContextKeyListeners()); } protected createMainEditorPart(): MainEditorPart { return this.instantiationService.createInstance(MainEditorPart, this); } + //#region Scoped Instantiation Services + + private readonly mapPartToInstantiationService = new Map(); + + getScopedInstantiationService(part: IEditorPart): IInstantiationService { + if (part === this.mainPart) { + if (!this.mapPartToInstantiationService.has(part.windowId)) { + this.instantiationService.invokeFunction(accessor => { + const editorService = accessor.get(IEditorService); // using `invokeFunction` to get hold of `IEditorService` lazily + + this.mapPartToInstantiationService.set(part.windowId, this._register(this.instantiationService.createChild(new ServiceCollection( + [IEditorService, editorService.createScoped('main', this._store)] + )))); + }); + } + } + + return this.mapPartToInstantiationService.get(part.windowId) ?? this.instantiationService; + } + + //#endregion + //#region Auxiliary Editor Parts - private readonly _onDidCreateAuxiliaryEditorPart = this._register(new Emitter()); + private readonly _onDidCreateAuxiliaryEditorPart = this._register(new Emitter()); readonly onDidCreateAuxiliaryEditorPart = this._onDidCreateAuxiliaryEditorPart.event; async createAuxiliaryEditorPart(options?: IAuxiliaryEditorPartOpenOptions): Promise { const { part, instantiationService, disposables } = await this.instantiationService.createInstance(AuxiliaryEditorPart, this).create(this.getGroupsLabel(this._parts.size), options); + // Keep instantiation service + this.mapPartToInstantiationService.set(part.windowId, instantiationService); + disposables.add(toDisposable(() => this.mapPartToInstantiationService.delete(part.windowId))); + // Events this._onDidAddGroup.fire(part.activeGroup); - const eventDisposables = disposables.add(new DisposableStore()); - this._onDidCreateAuxiliaryEditorPart.fire({ part, instantiationService, disposables: eventDisposables }); + this._onDidCreateAuxiliaryEditorPart.fire(part); return part; } @@ -154,7 +189,7 @@ export class EditorParts extends MultiWindowParts implements IEditor override getPart(element: HTMLElement): EditorPart; override getPart(groupOrElement: IEditorGroupView | GroupIdentifier | HTMLElement): EditorPart { if (this._parts.size > 1) { - if (groupOrElement instanceof HTMLElement) { + if (isHTMLElement(groupOrElement)) { const element = groupOrElement; return this.getPartByDocument(element.ownerDocument); @@ -242,29 +277,11 @@ export class EditorParts extends MultiWindowParts implements IEditor private createState(): IEditorPartsUIState { return { auxiliary: this.parts.filter(part => part !== this.mainPart).map(part => { + const auxiliaryWindow = this.auxiliaryWindowService.getWindow(part.windowId); + return { state: part.createState(), - bounds: (() => { - const auxiliaryWindow = getWindow(part.getContainer()); - if (auxiliaryWindow) { - return { - x: auxiliaryWindow.screenX, - y: auxiliaryWindow.screenY, - width: auxiliaryWindow.outerWidth, - height: auxiliaryWindow.outerHeight - }; - } - - return undefined; - })(), - zoomLevel: (() => { - const auxiliaryWindow = getWindow(part.getContainer()); - if (auxiliaryWindow) { - return getZoomLevel(auxiliaryWindow); - } - - return undefined; - })() + ...auxiliaryWindow?.createState() }; }), mru: this.mostRecentActiveParts.map(part => this.parts.indexOf(part)) @@ -277,11 +294,7 @@ export class EditorParts extends MultiWindowParts implements IEditor // Create auxiliary editor parts for (const auxiliaryEditorPartState of state.auxiliary) { - auxiliaryEditorPartPromises.push(this.createAuxiliaryEditorPart({ - bounds: auxiliaryEditorPartState.bounds, - state: auxiliaryEditorPartState.state, - zoomLevel: auxiliaryEditorPartState.zoomLevel - })); + auxiliaryEditorPartPromises.push(this.createAuxiliaryEditorPart(auxiliaryEditorPartState)); } // Await creation @@ -314,32 +327,132 @@ export class EditorParts extends MultiWindowParts implements IEditor } } - private async applyState(state: IEditorPartsUIState): Promise { + private async applyState(state: IEditorPartsUIState | 'empty'): Promise { + + // Before closing windows, try to close as many editors as + // possible, but skip over those that would trigger a dialog + // (for example when being dirty). This is to be able to have + // them merge into the main part. - // Close all editors and auxiliary parts first for (const part of this.parts) { if (part === this.mainPart) { continue; // main part takes care on its own } for (const group of part.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { - const closed = await group.closeAllEditors(); - if (!closed) { - return false; - } + await group.closeAllEditors({ excludeConfirming: true }); } - (part as unknown as IAuxiliaryEditorPart).close(); + const closed = (part as unknown as IAuxiliaryEditorPart).close(); // will move remaining editors to main part + if (!closed) { + return false; // this indicates that closing was vetoed + } } - // Restore auxiliary state - await this.restoreState(state); + // Restore auxiliary state unless we are in an empty state + if (state !== 'empty') { + await this.restoreState(state); + } return true; } //#endregion + //#region Working Sets + + private static readonly EDITOR_WORKING_SETS_STORAGE_KEY = 'editor.workingSets'; + + private editorWorkingSets: IEditorWorkingSetState[] = (() => { + const workingSetsRaw = this.storageService.get(EditorParts.EDITOR_WORKING_SETS_STORAGE_KEY, StorageScope.WORKSPACE); + if (workingSetsRaw) { + return JSON.parse(workingSetsRaw); + } + + return []; + })(); + + saveWorkingSet(name: string): IEditorWorkingSet { + const workingSet: IEditorWorkingSetState = { + id: generateUuid(), + name, + main: this.mainPart.createState(), + auxiliary: this.createState() + }; + + this.editorWorkingSets.push(workingSet); + + this.saveWorkingSets(); + + return { + id: workingSet.id, + name: workingSet.name + }; + } + + getWorkingSets(): IEditorWorkingSet[] { + return this.editorWorkingSets.map(workingSet => ({ id: workingSet.id, name: workingSet.name })); + } + + deleteWorkingSet(workingSet: IEditorWorkingSet): void { + const index = this.indexOfWorkingSet(workingSet); + if (typeof index === 'number') { + this.editorWorkingSets.splice(index, 1); + + this.saveWorkingSets(); + } + } + + async applyWorkingSet(workingSet: IEditorWorkingSet | 'empty', options?: IEditorWorkingSetOptions): Promise { + let workingSetState: IEditorWorkingSetState | 'empty' | undefined; + if (workingSet === 'empty') { + workingSetState = 'empty'; + } else { + workingSetState = this.editorWorkingSets[this.indexOfWorkingSet(workingSet) ?? -1]; + } + + if (!workingSetState) { + return false; + } + + // Apply state: begin with auxiliary windows first because it helps to keep + // editors around that need confirmation by moving them into the main part. + // Also, in rare cases, the auxiliary part may not be able to apply the state + // for certain editors that cannot move to the main part. + const applied = await this.applyState(workingSetState === 'empty' ? workingSetState : workingSetState.auxiliary); + if (!applied) { + return false; + } + await this.mainPart.applyState(workingSetState === 'empty' ? workingSetState : workingSetState.main, options); + + // Restore Focus unless instructed otherwise + if (!options?.preserveFocus) { + const mostRecentActivePart = firstOrDefault(this.mostRecentActiveParts); + if (mostRecentActivePart) { + await mostRecentActivePart.whenReady; + mostRecentActivePart.activeGroup.focus(); + } + } + + return true; + } + + private indexOfWorkingSet(workingSet: IEditorWorkingSet): number | undefined { + for (let i = 0; i < this.editorWorkingSets.length; i++) { + if (this.editorWorkingSets[i].id === workingSet.id) { + return i; + } + } + + return undefined; + } + + private saveWorkingSets(): void { + this.storageService.store(EditorParts.EDITOR_WORKING_SETS_STORAGE_KEY, JSON.stringify(this.editorWorkingSets), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + //#endregion + //#region Events private readonly _onDidActiveGroupChange = this._register(new Emitter()); @@ -368,7 +481,7 @@ export class EditorParts extends MultiWindowParts implements IEditor //#endregion - //#region Editor Groups Service + //#region Group Management get activeGroup(): IEditorGroupView { return this.activePart.activeGroup; @@ -445,16 +558,16 @@ export class EditorParts extends MultiWindowParts implements IEditor this.getPart(group).setSize(group, size); } - arrangeGroups(arrangement: GroupsArrangement, group?: IEditorGroupView | GroupIdentifier): void { - (group !== undefined ? this.getPart(group) : this.activePart).arrangeGroups(arrangement, group); + arrangeGroups(arrangement: GroupsArrangement, group: IEditorGroupView | GroupIdentifier = this.activePart.activeGroup): void { + this.getPart(group).arrangeGroups(arrangement, group); } - toggleMaximizeGroup(group?: IEditorGroupView | GroupIdentifier): void { - (group !== undefined ? this.getPart(group) : this.activePart).toggleMaximizeGroup(group); + toggleMaximizeGroup(group: IEditorGroupView | GroupIdentifier = this.activePart.activeGroup): void { + this.getPart(group).toggleMaximizeGroup(group); } - toggleExpandGroup(group?: IEditorGroupView | GroupIdentifier): void { - (group !== undefined ? this.getPart(group) : this.activePart).toggleExpandGroup(group); + toggleExpandGroup(group: IEditorGroupView | GroupIdentifier = this.activePart.activeGroup): void { + this.getPart(group).toggleExpandGroup(group); } restoreGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { @@ -531,11 +644,11 @@ export class EditorParts extends MultiWindowParts implements IEditor return this.getPart(group).moveGroup(group, location, direction); } - mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroupView { + mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): boolean { return this.getPart(group).mergeGroup(group, target, options); } - mergeAllGroups(target: IEditorGroupView | GroupIdentifier): IEditorGroupView { + mergeAllGroups(target: IEditorGroupView | GroupIdentifier): boolean { return this.activePart.mergeAllGroups(target); } @@ -549,6 +662,147 @@ export class EditorParts extends MultiWindowParts implements IEditor //#endregion + //#region Editor Group Context Key Handling + + private readonly globalContextKeys = new Map>(); + private readonly scopedContextKeys = new Map>>(); + + private registerGroupsContextKeyListeners(): void { + this._register(this.onDidChangeActiveGroup(() => this.updateGlobalContextKeys())); + this.groups.forEach(group => this.registerGroupContextKeyProvidersListeners(group)); + this._register(this.onDidAddGroup(group => this.registerGroupContextKeyProvidersListeners(group))); + this._register(this.onDidRemoveGroup(group => { + this.scopedContextKeys.delete(group.id); + this.registeredContextKeys.delete(group.id); + this.contextKeyProviderDisposables.deleteAndDispose(group.id); + })); + } + + private updateGlobalContextKeys(): void { + const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id); + if (!activeGroupScopedContextKeys) { + return; + } + + for (const [key, globalContextKey] of this.globalContextKeys) { + const scopedContextKey = activeGroupScopedContextKeys.get(key); + if (scopedContextKey) { + globalContextKey.set(scopedContextKey.get()); + } else { + globalContextKey.reset(); + } + } + } + + bind(contextKey: RawContextKey, group: IEditorGroupView): IContextKey { + + // Ensure we only bind to the same context key once globaly + let globalContextKey = this.globalContextKeys.get(contextKey.key); + if (!globalContextKey) { + globalContextKey = contextKey.bindTo(this.contextKeyService); + this.globalContextKeys.set(contextKey.key, globalContextKey); + } + + // Ensure we only bind to the same context key once per group + let groupScopedContextKeys = this.scopedContextKeys.get(group.id); + if (!groupScopedContextKeys) { + groupScopedContextKeys = new Map>(); + this.scopedContextKeys.set(group.id, groupScopedContextKeys); + } + let scopedContextKey = groupScopedContextKeys.get(contextKey.key); + if (!scopedContextKey) { + scopedContextKey = contextKey.bindTo(group.scopedContextKeyService); + groupScopedContextKeys.set(contextKey.key, scopedContextKey); + } + + const that = this; + return { + get(): T | undefined { + return scopedContextKey.get() as T | undefined; + }, + set(value: T): void { + if (that.activeGroup === group) { + globalContextKey.set(value); + } + scopedContextKey.set(value); + }, + reset(): void { + if (that.activeGroup === group) { + globalContextKey.reset(); + } + scopedContextKey.reset(); + }, + }; + } + + private readonly contextKeyProviders = new Map>(); + private readonly registeredContextKeys = new Map>(); + + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable { + if (this.contextKeyProviders.has(provider.contextKey.key) || this.globalContextKeys.has(provider.contextKey.key)) { + throw new Error(`A context key provider for key ${provider.contextKey.key} already exists.`); + } + + this.contextKeyProviders.set(provider.contextKey.key, provider); + + const setContextKeyForGroups = () => { + for (const group of this.groups) { + this.updateRegisteredContextKey(group, provider); + } + }; + + // Run initially and on change + setContextKeyForGroups(); + const onDidChange = provider.onDidChange?.(() => setContextKeyForGroups()); + + return toDisposable(() => { + onDidChange?.dispose(); + + this.globalContextKeys.delete(provider.contextKey.key); + this.scopedContextKeys.forEach(scopedContextKeys => scopedContextKeys.delete(provider.contextKey.key)); + + this.contextKeyProviders.delete(provider.contextKey.key); + this.registeredContextKeys.forEach(registeredContextKeys => registeredContextKeys.delete(provider.contextKey.key)); + }); + } + + private readonly contextKeyProviderDisposables = this._register(new DisposableMap()); + private registerGroupContextKeyProvidersListeners(group: IEditorGroupView): void { + + // Update context keys from providers for the group when its active editor changes + const disposable = group.onDidActiveEditorChange(() => { + for (const contextKeyProvider of this.contextKeyProviders.values()) { + this.updateRegisteredContextKey(group, contextKeyProvider); + } + }); + + this.contextKeyProviderDisposables.set(group.id, disposable); + } + + private updateRegisteredContextKey(group: IEditorGroupView, provider: IEditorGroupContextKeyProvider): void { + + // Get the group scoped context keys for the provider + // If the providers context key has not yet been bound + // to the group, do so now. + + let groupRegisteredContextKeys = this.registeredContextKeys.get(group.id); + if (!groupRegisteredContextKeys) { + groupRegisteredContextKeys = new Map(); + this.registeredContextKeys.set(group.id, groupRegisteredContextKeys); + } + + let scopedRegisteredContextKey = groupRegisteredContextKeys.get(provider.contextKey.key); + if (!scopedRegisteredContextKey) { + scopedRegisteredContextKey = this.bind(provider.contextKey, group); + groupRegisteredContextKeys.set(provider.contextKey.key, scopedRegisteredContextKey); + } + + // Set the context key value for the group context + scopedRegisteredContextKey.set(provider.getGroupContextKeyValue(group)); + } + + //#endregion + //#region Main Editor Part Only get partOptions() { return this.mainPart.partOptions; } diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index 7a7691b3850..e434679f445 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -52,7 +52,7 @@ export abstract class EditorPlaceholder extends EditorPane { private container: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; - private inputDisposable = this._register(new MutableDisposable()); + private readonly inputDisposable = this._register(new MutableDisposable()); constructor( id: string, @@ -185,7 +185,7 @@ export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor'; private static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); + static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, this.ID, this.LABEL); constructor( group: IEditorGroup, @@ -223,7 +223,7 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { private static readonly ID = 'workbench.editors.errorEditor'; private static readonly LABEL = localize('errorEditor', "Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); + static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, this.ID, this.LABEL); constructor( group: IEditorGroup, diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index 225330438f6..3a69dbbff43 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -60,7 +60,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro ); } - override provide(picker: IQuickPick, token: CancellationToken): IDisposable { + override provide(picker: IQuickPick, token: CancellationToken): IDisposable { // Reset the pick state for this run this.pickState.reset(!!picker.quickNavigate); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index dc58c035fed..dcef2cf97d9 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -55,9 +55,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { TabFocus } from 'vs/editor/browser/config/tabFocus'; -import { mainWindow } from 'vs/base/browser/window'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorGroupsService, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; class SideBySideEditorEncodingSupport implements IEncodingSupport { constructor(private primary: IEncodingSupport, private secondary: IEncodingSupport) { } @@ -886,22 +884,23 @@ export class EditorStatusContribution extends Disposable implements IWorkbenchCo static readonly ID = 'workbench.contrib.editorStatus'; constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, ) { super(); - // Main Editor Status - const mainInstantiationService = instantiationService.createChild(new ServiceCollection( - [IEditorService, editorService.createScoped('main', this._store)] - )); - this._register(mainInstantiationService.createInstance(EditorStatus, mainWindow.vscodeWindowId)); + for (const part of editorGroupService.parts) { + this.createEditorStatus(part); + } - // Auxiliary Editor Status - this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(({ part, instantiationService, disposables }) => { - disposables.add(instantiationService.createInstance(EditorStatus, part.windowId)); - })); + this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.createEditorStatus(part))); + } + + private createEditorStatus(part: IEditorPart): void { + const disposables = new DisposableStore(); + Event.once(part.onWillDispose)(() => disposables.dispose()); + + const scopedInstantiationService = this.editorGroupService.getScopedInstantiationService(part); + disposables.add(scopedInstantiationService.createInstance(EditorStatus, part.windowId)); } } @@ -1079,11 +1078,20 @@ export class ChangeLanguageAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyM) }, - precondition: ContextKeyExpr.not('notebookEditorFocused') + precondition: ContextKeyExpr.not('notebookEditorFocused'), + metadata: { + description: localize('changeLanguageMode.description', "Change the language mode of the active text editor."), + args: [ + { + name: localize('changeLanguageMode.arg.name', "The name of the language mode to change to."), + constraint: (value: any) => typeof value === 'string', + } + ] + } }); } - override async run(accessor: ServicesAccessor): Promise { + override async run(accessor: ServicesAccessor, languageMode?: string): Promise { const quickInputService = accessor.get(IQuickInputService); const editorService = accessor.get(IEditorService); const languageService = accessor.get(ILanguageService); @@ -1162,7 +1170,7 @@ export class ChangeLanguageAction extends Action2 { }; picks.unshift(autoDetectLanguage); - const pick = await quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); + const pick = typeof languageMode === 'string' ? { label: languageMode } : await quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); if (!pick) { return; } @@ -1444,13 +1452,16 @@ export class ChangeEncodingAction extends Action2 { let guessedEncoding: string | undefined = undefined; if (fileService.hasProvider(resource)) { - const content = await textFileService.readStream(resource, { autoGuessEncoding: true }); + const content = await textFileService.readStream(resource, { + autoGuessEncoding: true, + candidateGuessEncodings: textResourceConfigurationService.getValue(resource, 'files.candidateGuessEncodings') + }); guessedEncoding = content.encoding; } const isReopenWithEncoding = (action === reopenWithEncodingPick); - const configuredEncoding = textResourceConfigurationService.getValue(resource ?? undefined, 'files.encoding'); + const configuredEncoding = textResourceConfigurationService.getValue(resource, 'files.encoding'); let directMatchIndex: number | undefined; let aliasMatchIndex: number | undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index d7a82ed5c16..e5e4f7774de 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -86,6 +86,7 @@ export interface IEditorTabsControl extends IDisposable { stickEditor(editor: EditorInput): void; unstickEditor(editor: EditorInput): void; setActive(isActive: boolean): void; + updateEditorSelections(): void; updateEditorLabel(editor: EditorInput): void; updateEditorDirty(editor: EditorInput): void; layout(dimensions: IEditorTitleControlDimensions): Dimension; @@ -145,9 +146,9 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC super(themeService); this.contextMenuContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, this.contextMenuContextKeyService], - )); + ))); this.resourceContext = this._register(scopedInstantiationService.createInstance(ResourceContextKey)); @@ -502,6 +503,8 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC abstract setActive(isActive: boolean): void; + abstract updateEditorSelections(): void; + abstract updateEditorLabel(editor: EditorInput): void; abstract updateEditorDirty(editor: EditorInput): void; diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index 3f14abf5b9e..d134e9b6175 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -36,10 +36,10 @@ export interface IEditorTitleControlDimensions { export class EditorTitleControl extends Themable { private editorTabsControl: IEditorTabsControl; - private editorTabsControlDisposable = this._register(new DisposableStore()); + private readonly editorTabsControlDisposable = this._register(new DisposableStore()); private breadcrumbsControlFactory: BreadcrumbsControlFactory | undefined; - private breadcrumbsControlDisposables = this._register(new DisposableStore()); + private readonly breadcrumbsControlDisposables = this._register(new DisposableStore()); private get breadcrumbsControl() { return this.breadcrumbsControlFactory?.control; } constructor( @@ -163,6 +163,10 @@ export class EditorTitleControl extends Themable { return this.editorTabsControl.setActive(isActive); } + updateEditorSelections(): void { + this.editorTabsControl.updateEditorSelections(); + } + updateEditorLabel(editor: EditorInput): void { return this.editorTabsControl.updateEditorLabel(editor); } diff --git a/src/vs/workbench/browser/parts/editor/media/editorquickaccess.css b/src/vs/workbench/browser/parts/editor/media/editorquickaccess.css index 89390b0c7e5..2e25284a83e 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorquickaccess.css +++ b/src/vs/workbench/browser/parts/editor/media/editorquickaccess.css @@ -4,5 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-editor::before { - content: "\ea76"; /* Close icon flips between black dot and "X" for dirty open editors */ + /* Close icon flips between black dot and "X" for dirty open editors */ + content: var(--vscode-icon-x-content); + font-family: var(--vscode-icon-x-font-family); } diff --git a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css index bf44c02aee2..24de0a1f384 100644 --- a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css @@ -46,4 +46,12 @@ border-radius: 10px; font-size: 12px; position: absolute; + /* + * Browsers apply an effect to the drag image when the div becomes too + * large which makes them unreadable. Use max width so it does not happen + */ + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 18e66d6a1df..93559402aa5 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -106,6 +106,43 @@ padding-left: 10px; } +/* Tab Background Color in/active group/tab */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { + background-color: var(--vscode-tab-unfocusedInactiveBackground); +} +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab { + background-color: var(--vscode-tab-inactiveBackground); +} +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + background-color: var(--vscode-tab-unfocusedActiveBackground); +} +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active { + background-color: var(--vscode-tab-activeBackground); +} + +/* Tab Foreground Color in/active group/tab */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { + color: var(--vscode-tab-unfocusedInactiveForeground); +} +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab { + color: var(--vscode-tab-inactiveForeground); +} +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + color: var(--vscode-tab-unfocusedActiveForeground); +} +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active { + color: var(--vscode-tab-activeForeground); +} + +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.selected:not(.active) { + background-color: var(--vscode-tab-selectedBackground); + color: var(--vscode-tab-selectedForeground); +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.active) { + box-shadow: none; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */ } @@ -204,11 +241,14 @@ left: unset !important; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left::after, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-action-off::after, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed.tab-actions-left::after, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed.close-action-off::after { - content: ''; +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-fade-hider { + display: none; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left .tab-fade-hider, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-action-off .tab-fade-hider, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed.tab-actions-left .tab-fade-hider, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed.close-action-off .tab-fade-hider { display: flex; flex: 0; width: 5px; /* reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ @@ -234,6 +274,7 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected.tab-border-top > .tab-border-top-container, .monaco-workbench .part.editor > .content .editor-group-container > .title:not(.two-tab-bars) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars .tabs-and-actions-container:not(:first-child) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { @@ -244,7 +285,8 @@ width: 100%; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected.tab-border-top > .tab-border-top-container { z-index: 6; top: 0; height: 1px; @@ -385,14 +427,16 @@ .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.sticky.dirty > .tab-actions .action-label:not(:hover)::before, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky.dirty > .tab-actions .action-label:not(:hover)::before { - content: "\ebb2"; /* use `pinned-dirty` icon unicode for sticky-dirty indication */ - font-family: 'codicon'; + /* use `pinned-dirty` icon unicode for sticky-dirty indication */ + content: var(--vscode-icon-pinned-dirty-content); + font-family: var(--vscode-icon-pinned-dirty-font-family); } .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-actions .action-label:not(:hover)::before, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label:not(:hover)::before { - content: "\ea71"; /* use `circle-filled` icon unicode for dirty indication */ - font-family: 'codicon'; + /* use `circle-filled` icon unicode for dirty indication */ + content: var(--vscode-icon-circle-filled-content); + font-family: var(--vscode-icon-circle-filled-font-family); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-actions .action-label, diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 0043df4e615..9007f132a8e 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/multieditortabscontrol'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { shorten } from 'vs/base/common/labels'; import { EditorResourceAccessor, Verbosity, IEditorPartOptions, SideBySideEditor, DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IUntypedEditorInput, preventEditorClose, EditorCloseMethod, EditorsOrder, IToolbarActions } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -26,8 +26,8 @@ import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElemen import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; -import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER, TAB_SELECTED_BORDER_TOP } from 'vs/workbench/common/theme'; +import { activeContrastBorder, contrastBorder, editorBackground, listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, extractTreeDropData, isWindowDraggedOver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -35,7 +35,7 @@ import { MergeGroupMode, IMergeGroupOptions } from 'vs/workbench/services/editor import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; -import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; +import { CloseEditorTabAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { basenameOrAuthority } from 'vs/base/common/resources'; @@ -56,6 +56,8 @@ import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor import { StickyEditorGroupModel, UnstickyEditorGroupModel } from 'vs/workbench/common/editor/filteredEditorGroupModel'; import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { applyDragImage } from 'vs/base/browser/dnd'; interface IEditorInputLabel { readonly editor: EditorInput; @@ -109,7 +111,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private tabsScrollbar: ScrollableElement | undefined; private tabSizingFixedDisposables: DisposableStore | undefined; - private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL)); + private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL)); private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL)); private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); @@ -316,6 +318,15 @@ export class MultiEditorTabsControl extends EditorTabsControl { } })); + // Prevent auto-pasting (https://github.com/microsoft/vscode/issues/201696) + if (isLinux) { + this._register(addDisposableListener(tabsContainer, EventType.MOUSE_UP, e => { + if (e.button === 1) { + e.preventDefault(); + } + })); + } + // Drag & Drop support let lastDragEvent: DragEvent | undefined = undefined; let isNewWindowOperation = false; @@ -665,7 +676,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Activity has an impact on each tab's active indication this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { - this.redrawTabActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); + this.redrawTabSelectedActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); }); // Activity has an impact on the toolbar, so we need to update and layout @@ -673,6 +684,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.layout(this.dimensions, { forceRevealActiveTab: true }); } + updateEditorSelections(): void { + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { + this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); + }); + } + private updateEditorLabelScheduler = this._register(new RunOnceScheduler(() => this.doUpdateEditorLabels(), 0)); updateEditorLabel(editor: EditorInput): void { @@ -700,7 +717,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } updateEditorDirty(editor: EditorInput): void { - this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar)); + this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar)); } override updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { @@ -716,6 +733,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.updateTabsScrollbarSizing(); } + // Update editor actions + if (oldOptions.alwaysShowEditorActions !== newOptions.alwaysShowEditorActions) { + this.updateEditorActionsToolbar(); + } + // Update tabs sizing if ( oldOptions.tabSizingFixedMinWidth !== newOptions.tabSizingFixedMinWidth || @@ -815,6 +837,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { const tabActionBarDisposable = combinedDisposable(tabActionBar, tabActionListener, toDisposable(insert(this.tabActionBars, tabActionBar))); + // Tab Fade Hider + // Hides the tab fade to the right when tab action left and sizing shrink/fixed, ::after, ::before are already used + const tabShadowHider = document.createElement('div'); + tabShadowHider.classList.add('tab-fade-hider'); + tabContainer.appendChild(tabShadowHider); + // Tab Border Bottom const tabBorderBottomContainer = document.createElement('div'); tabBorderBottomContainer.classList.add('tab-border-bottom-container'); @@ -838,10 +866,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { return this.groupView.getIndexOfEditor(editor); } + private lastSingleSelectSelectedEditor: EditorInput | undefined; private registerTabListeners(tab: HTMLElement, tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable { const disposables = new DisposableStore(); - const handleClickOrTouch = (e: MouseEvent | GestureEvent, preserveFocus: boolean): void => { + const handleClickOrTouch = async (e: MouseEvent | GestureEvent, preserveFocus: boolean): Promise => { tab.blur(); // prevent flicker of focus outline on tab until editor got focus if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { @@ -849,7 +878,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { e.preventDefault(); // required to prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) } - return undefined; + return; } if (this.originatesFromTabActionBar(e)) { @@ -859,11 +888,34 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Open tabs editor const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - // Even if focus is preserved make sure to activate the group. - this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }); + if (e.shiftKey) { + let anchor: EditorInput; + if (this.lastSingleSelectSelectedEditor && this.tabsModel.isSelected(this.lastSingleSelectSelectedEditor)) { + // The last selected editor is the anchor + anchor = this.lastSingleSelectSelectedEditor; + } else { + // The active editor is the anchor + const activeEditor = assertIsDefined(this.groupView.activeEditor); + this.lastSingleSelectSelectedEditor = activeEditor; + anchor = activeEditor; + } + await this.selectEditorsBetween(editor, anchor); + } else if ((e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh)) { + if (this.tabsModel.isSelected(editor)) { + await this.unselectEditor(editor); + } else { + await this.selectEditor(editor); + this.lastSingleSelectSelectedEditor = editor; + } + } else { + // Even if focus is preserved make sure to activate the group. + // If a new active editor is selected, keep the current selection on key + // down such that drag and drop can operate over the selection. The selection + // is removed on key up in this case. + const inactiveSelection = this.tabsModel.isSelected(editor) ? this.groupView.selectedEditors.filter(e => !e.matches(editor)) : []; + await this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }, { inactiveSelection, focusTabControl: true }); + } } - - return undefined; }; const showContextMenu = (e: Event) => { @@ -884,11 +936,24 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabsScrollbar.setScrollPosition({ scrollLeft: tabsScrollbar.getScrollPosition().scrollLeft - e.translationX }); })); - // Prevent flicker of focus outline on tab until editor got focus - disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, e => { + // Update selection & prevent flicker of focus outline on tab until editor got focus + disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, async e => { EventHelper.stop(e); tab.blur(); + + if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { + return; + } + + if (this.originatesFromTabActionBar(e)) { + return; // not when clicking on actions + } + + const isCtrlCmd = (e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh); + if (!isCtrlCmd && !e.shiftKey && this.groupView.selectedEditors.length > 1) { + await this.unselectAllEditors(); + } })); // Close on mouse middle click @@ -1014,16 +1079,21 @@ export class MultiEditorTabsControl extends EditorTabsControl { } isNewWindowOperation = this.isNewWindowOperation(e); - - this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, groupId: this.groupView.id })], DraggedEditorIdentifier.prototype); + const selectedEditors = this.groupView.selectedEditors; + this.editorTransfer.setData(selectedEditors.map(e => new DraggedEditorIdentifier({ editor: e, groupId: this.groupView.id })), DraggedEditorIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; - e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback + if (selectedEditors.length > 1) { + const label = `${editor.getName()} + ${selectedEditors.length - 1}`; + applyDragImage(e, label, 'monaco-editor-group-drag-image', this.getColor(listActiveSelectionBackground), this.getColor(listActiveSelectionForeground)); + } else { + e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback + } } // Apply some datatransfer types to allow for dragging the element outside of the application - this.doFillResourceDataTransfers([editor], e, isNewWindowOperation); + this.doFillResourceDataTransfers(selectedEditors, e, isNewWindowOperation); scheduleAtNextAnimationFrame(getWindow(this.parent), () => this.updateDropFeedback(tab, false, e, tabIndex)); }, @@ -1067,14 +1137,14 @@ export class MultiEditorTabsControl extends EditorTabsControl { onDragEnd: async e => { this.updateDropFeedback(tab, false, e, tabIndex); - + const draggedEditors = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); - const editor = this.tabsModel.getEditorByIndex(tabIndex); if ( !isNewWindowOperation || isWindowDraggedOver() || - !editor + !draggedEditors || + draggedEditors.length === 0 ) { return; // drag to open in new window is disabled } @@ -1085,10 +1155,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { } const targetGroup = auxiliaryEditorPart.activeGroup; - if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, editor)) { - this.groupView.moveEditor(editor, targetGroup); + const editors = draggedEditors.map(de => ({ editor: de.identifier.editor })); + if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, draggedEditors[0].identifier.editor)) { + this.groupView.moveEditors(editors, targetGroup); } else { - this.groupView.copyEditor(editor, targetGroup); + this.groupView.copyEditors(editors, targetGroup); } targetGroup.focus(); @@ -1103,22 +1174,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { targetIndex++; } - // If we are moving an editor inside the same group and it is - // located before the target index we need to reduce the index - // by one to account for the fact that the move will cause all - // subsequent tabs to move one to the left. - const editorIdentifiers = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (editorIdentifiers !== undefined) { - const draggedEditorIdentifier = editorIdentifiers[0].identifier; - const sourceGroup = this.editorPartsView.getGroup(draggedEditorIdentifier.groupId); - if (sourceGroup?.id === this.groupView.id) { - const editorIndex = sourceGroup.getIndexOfEditor(draggedEditorIdentifier.editor); - if (editorIndex < targetIndex) { - targetIndex--; - } - } - } - this.onDrop(e, targetIndex, tabsContainer); } })); @@ -1129,7 +1184,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private isSupportedDropTransfer(e: DragEvent): boolean { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const group = data[0]; if (group.identifier === this.groupView.id) { return false; // groups cannot be dropped on group it originates from @@ -1219,6 +1274,93 @@ export class MultiEditorTabsControl extends EditorTabsControl { return { leftElement: tabBefore as HTMLElement, rightElement: tabAfter as HTMLElement }; } + private async selectEditor(editor: EditorInput): Promise { + if (this.groupView.isActive(editor)) { + return; + } + + await this.groupView.setSelection(editor, this.groupView.selectedEditors); + } + + private async selectEditorsBetween(target: EditorInput, anchor: EditorInput): Promise { + const editorIndex = this.groupView.getIndexOfEditor(target); + if (editorIndex === -1) { + throw new BugIndicatingError(); + } + + const anchorEditorIndex = this.groupView.getIndexOfEditor(anchor); + if (anchorEditorIndex === -1) { + throw new BugIndicatingError(); + } + + let selection = this.groupView.selectedEditors; + + // Unselect editors on other side of anchor in relation to the target + let currentEditorIndex = anchorEditorIndex; + while (currentEditorIndex >= 0 && currentEditorIndex <= this.groupView.count - 1) { + currentEditorIndex = anchorEditorIndex < editorIndex ? currentEditorIndex - 1 : currentEditorIndex + 1; + + const currentEditor = this.groupView.getEditorByIndex(currentEditorIndex); + if (!currentEditor) { + break; + } + + if (!this.groupView.isSelected(currentEditor)) { + break; + } + + selection = selection.filter(editor => !editor.matches(currentEditor)); + } + + // Select editors between anchor and target + const fromEditorIndex = anchorEditorIndex < editorIndex ? anchorEditorIndex : editorIndex; + const toEditorIndex = anchorEditorIndex < editorIndex ? editorIndex : anchorEditorIndex; + + const editorsToSelect = this.groupView.getEditors(EditorsOrder.SEQUENTIAL).slice(fromEditorIndex, toEditorIndex + 1); + for (const editor of editorsToSelect) { + if (!this.groupView.isSelected(editor)) { + selection.push(editor); + } + } + + const inactiveSelectedEditors = selection.filter(editor => !editor.matches(target)); + await this.groupView.setSelection(target, inactiveSelectedEditors); + } + + private async unselectEditor(editor: EditorInput): Promise { + const isUnselectingActiveEditor = this.groupView.isActive(editor); + + // If there is only one editor selected, do not unselect it + if (isUnselectingActiveEditor && this.groupView.selectedEditors.length === 1) { + return; + } + + let newActiveEditor = assertIsDefined(this.groupView.activeEditor); + + // If active editor is bing unselected then find the most recently opened selected editor + // that is not the editor being unselected + if (isUnselectingActiveEditor) { + const recentEditors = this.groupView.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + for (let i = 1; i < recentEditors.length; i++) { // First one is the active editor + const recentEditor = recentEditors[i]; + if (this.groupView.isSelected(recentEditor)) { + newActiveEditor = recentEditor; + break; + } + } + } + + const inactiveSelectedEditors = this.groupView.selectedEditors.filter(e => !e.matches(editor) && !e.matches(newActiveEditor)); + await this.groupView.setSelection(newActiveEditor, inactiveSelectedEditors); + } + + private async unselectAllEditors(): Promise { + if (this.groupView.selectedEditors.length > 1) { + const activeEditor = assertIsDefined(this.groupView.activeEditor); + await this.groupView.setSelection(activeEditor, []); + } + } + private computeTabLabels(): void { const { labelFormat } = this.groupsView.partOptions; const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat); @@ -1436,8 +1578,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Borders / outline this.redrawTabBorders(tabIndex, tabContainer); - // Active / dirty state - this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); + // Selection / active / dirty state + this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); } private redrawTabLabel(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { @@ -1495,7 +1637,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } - private redrawTabActiveAndDirty(isGroupActive: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { + private redrawTabSelectedActiveAndDirty(isGroupActive: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { const isTabActive = this.tabsModel.isActive(editor); const hasModifiedBorderTop = this.doRedrawTabDirty(isGroupActive, isTabActive, editor, tabContainer); @@ -1503,57 +1645,36 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { + const isActive = this.tabsModel.isActive(editor); + const isSelected = this.tabsModel.isSelected(editor); - // Tab is active - if (this.tabsModel.isActive(editor)) { - - // Container - tabContainer.classList.add('active'); - tabContainer.setAttribute('aria-selected', 'true'); - tabContainer.tabIndex = 0; // Only active tab can be focused into - tabContainer.style.backgroundColor = this.getColor(isGroupActive ? TAB_ACTIVE_BACKGROUND : TAB_UNFOCUSED_ACTIVE_BACKGROUND) || ''; + tabContainer.classList.toggle('active', isActive); + tabContainer.classList.toggle('selected', isSelected); + tabContainer.setAttribute('aria-selected', isActive ? 'true' : 'false'); + tabContainer.tabIndex = isActive ? 0 : -1; // Only active tab can be focused into + tabActionBar.setFocusable(isActive); + // Set border BOTTOM if theme defined color + if (isActive) { const activeTabBorderColorBottom = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER); - if (activeTabBorderColorBottom) { - tabContainer.classList.add('tab-border-bottom'); - tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom.toString()); - } else { - tabContainer.classList.remove('tab-border-bottom'); - tabContainer.style.removeProperty('--tab-border-bottom-color'); - } - - const activeTabBorderColorTop = allowBorderTop ? this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP) : undefined; - if (activeTabBorderColorTop) { - tabContainer.classList.add('tab-border-top'); - tabContainer.style.setProperty('--tab-border-top-color', activeTabBorderColorTop.toString()); - } else { - tabContainer.classList.remove('tab-border-top'); - tabContainer.style.removeProperty('--tab-border-top-color'); - } - - // Label - tabContainer.style.color = this.getColor(isGroupActive ? TAB_ACTIVE_FOREGROUND : TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; - - // Actions - tabActionBar.setFocusable(true); + tabContainer.classList.toggle('tab-border-bottom', !!activeTabBorderColorBottom); + tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom ?? ''); } - // Tab is inactive - else { + // Set border TOP if theme defined color + let tabBorderColorTop: string | null = null; + if (allowBorderTop) { + if (isActive) { + tabBorderColorTop = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP); + } - // Container - tabContainer.classList.remove('active'); - tabContainer.setAttribute('aria-selected', 'false'); - tabContainer.tabIndex = -1; // Only active tab can be focused into - tabContainer.style.backgroundColor = this.getColor(isGroupActive ? TAB_INACTIVE_BACKGROUND : TAB_UNFOCUSED_INACTIVE_BACKGROUND) || ''; - tabContainer.style.boxShadow = ''; - - // Label - tabContainer.style.color = this.getColor(isGroupActive ? TAB_INACTIVE_FOREGROUND : TAB_UNFOCUSED_INACTIVE_FOREGROUND) || ''; - - // Actions - tabActionBar.setFocusable(false); + if (tabBorderColorTop === null && isSelected) { + tabBorderColorTop = this.getColor(TAB_SELECTED_BORDER_TOP); + } } + + tabContainer.classList.toggle('tab-border-top', !!tabBorderColorTop); + tabContainer.style.setProperty('--tab-border-top-color', tabBorderColorTop ?? ''); } private doRedrawTabDirty(isGroupActive: boolean, isTabActive: boolean, editor: EditorInput, tabContainer: HTMLElement): boolean { @@ -1619,7 +1740,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Inactive: only show "Unlock" and secondary actions else { return { - primary: editorActions.primary.filter(action => action.id === UNLOCK_GROUP_COMMAND_ID), + primary: this.groupsView.partOptions.alwaysShowEditorActions ? editorActions.primary : editorActions.primary.filter(action => action.id === UNLOCK_GROUP_COMMAND_ID), secondary: editorActions.secondary }; } @@ -2070,7 +2191,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.updateDropFeedback(tabsContainer, false, e, targetTabIndex); tabsContainer.classList.remove('scroll'); - const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex; + let targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex; const options: IEditorOptions = { sticky: this.tabsModel instanceof StickyEditorGroupModel && this.tabsModel.stickyCount === targetEditorIndex, index: targetEditorIndex @@ -2079,7 +2200,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const sourceGroup = this.editorPartsView.getGroup(data[0].identifier); if (sourceGroup) { const mergeGroupOptions: IMergeGroupOptions = { index: targetEditorIndex }; @@ -2098,31 +2219,42 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { - const draggedEditor = data[0].identifier; - const sourceGroup = this.editorPartsView.getGroup(draggedEditor.groupId); + if (Array.isArray(data) && data.length > 0) { + const sourceGroup = this.editorPartsView.getGroup(data[0].identifier.groupId); if (sourceGroup) { + for (const de of data) { + const editor = de.identifier.editor; - // Move editor to target position and index - if (this.isMoveOperation(e, draggedEditor.groupId, draggedEditor.editor)) { - sourceGroup.moveEditor(draggedEditor.editor, this.groupView, options); - } + // Only allow moving/copying from a single group source + if (sourceGroup.id !== de.identifier.groupId) { + continue; + } - // Copy editor to target position and index - else { - sourceGroup.copyEditor(draggedEditor.editor, this.groupView, options); + // Keep the same order when moving / copying editors within the same group + const sourceEditorIndex = sourceGroup.getIndexOfEditor(editor); + if (sourceGroup === this.groupView && sourceEditorIndex < targetEditorIndex) { + targetEditorIndex--; + } + + if (this.isMoveOperation(e, de.identifier.groupId, editor)) { + sourceGroup.moveEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } else { + sourceGroup.copyEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } + + targetEditorIndex++; } } - - this.groupView.focus(); - this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } + + this.groupView.focus(); + this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } // Check for tree items else if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) { const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const editors: IUntypedEditorInput[] = []; for (const id of data) { const dataTransferItem = await this.treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier); @@ -2174,6 +2306,11 @@ registerThemingParticipant((theme, collector) => { outline-offset: -5px; } + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active):not(:hover) { + outline: 1px dotted; + outline-offset: -5px; + } + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:focus { outline-style: dashed; } @@ -2212,7 +2349,7 @@ registerThemingParticipant((theme, collector) => { const tabHoverBackground = theme.getColor(TAB_HOVER_BACKGROUND); if (tabHoverBackground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:not(.selected):hover { background-color: ${tabHoverBackground} !important; } `); @@ -2221,7 +2358,7 @@ registerThemingParticipant((theme, collector) => { const tabUnfocusedHoverBackground = theme.getColor(TAB_UNFOCUSED_HOVER_BACKGROUND); if (tabUnfocusedHoverBackground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.selected):hover { background-color: ${tabUnfocusedHoverBackground} !important; } `); @@ -2231,7 +2368,7 @@ registerThemingParticipant((theme, collector) => { const tabHoverForeground = theme.getColor(TAB_HOVER_FOREGROUND); if (tabHoverForeground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:not(.selected):hover { color: ${tabHoverForeground} !important; } `); @@ -2240,7 +2377,7 @@ registerThemingParticipant((theme, collector) => { const tabUnfocusedHoverForeground = theme.getColor(TAB_UNFOCUSED_HOVER_FOREGROUND); if (tabUnfocusedHoverForeground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.selected):hover { color: ${tabUnfocusedHoverForeground} !important; } `); diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 24d85415eb8..69eb8b77262 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -61,12 +61,8 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } openEditor(editor: EditorInput, options: IInternalEditorOpenOptions): boolean { - const [editorTabController, otherTabController] = this.model.isSticky(editor) ? [this.stickyEditorTabsControl, this.unstickyEditorTabsControl] : [this.unstickyEditorTabsControl, this.stickyEditorTabsControl]; - const didChange = editorTabController.openEditor(editor, options); + const didChange = this.getEditorTabsController(editor).openEditor(editor, options); if (didChange) { - // HACK: To render all editor tabs on startup, otherwise only one row gets rendered - otherTabController.openEditors([]); - this.handleOpenedEditors(); } return didChange; @@ -163,6 +159,11 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.setActive(isActive); } + updateEditorSelections(): void { + this.stickyEditorTabsControl.updateEditorSelections(); + this.unstickyEditorTabsControl.updateEditorSelections(); + } + updateEditorLabel(editor: EditorInput): void { this.getEditorTabsController(editor).updateEditorLabel(editor); } @@ -194,7 +195,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont return this.stickyEditorTabsControl.getHeight() + this.unstickyEditorTabsControl.getHeight(); } - public override dispose(): void { + override dispose(): void { this.parent.classList.toggle('two-tab-bars', false); super.dispose(); diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index baf08bb4504..6a0859cd3ec 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -61,6 +61,8 @@ export class NoEditorTabsControl extends EditorTabsControl { setActive(isActive: boolean): void { } + updateEditorSelections(): void { } + updateEditorLabel(editor: EditorInput): void { } updateEditorDirty(editor: EditorInput): void { } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index c3d7a0cce9d..2dab828b50a 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -160,7 +160,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState this.redraw()); } - stickEditor(editor: EditorInput): void { - // Sticky editors are not presented any different with tabs disabled - } + stickEditor(editor: EditorInput): void { } - unstickEditor(editor: EditorInput): void { - // Sticky editors are not presented any different with tabs disabled - } + unstickEditor(editor: EditorInput): void { } setActive(isActive: boolean): void { this.redraw(); } + updateEditorSelections(): void { } + updateEditorLabel(editor: EditorInput): void { this.ifEditorIsActive(editor, () => this.redraw()); } @@ -353,7 +351,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { // Inactive: only show "Close, "Unlock" and secondary actions else { return { - primary: editorActions.primary.filter(action => action.id === CLOSE_EDITOR_COMMAND_ID || action.id === UNLOCK_GROUP_COMMAND_ID), + primary: this.groupsView.partOptions.alwaysShowEditorActions ? editorActions.primary : editorActions.primary.filter(action => action.id === CLOSE_EDITOR_COMMAND_ID || action.id === UNLOCK_GROUP_COMMAND_ID), secondary: editorActions.secondary }; } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 3c298c26b1e..5743ac16f7a 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -341,7 +341,7 @@ export class TextDiffEditor extends AbstractTextEditor imp collapseUnchangedRegions: boolean; }, { owner: 'hediet'; - editorVisibleTimeMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Indicates the time the diff editor was visible to the user' }; + editorVisibleTimeMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates the time the diff editor was visible to the user' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates for which language the diff editor was shown' }; collapseUnchangedRegions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates whether unchanged regions were collapsed' }; comment: 'This event gives insight about how long the diff editor was visible to the user.'; diff --git a/src/vs/workbench/browser/parts/globalCompositeBar.ts b/src/vs/workbench/browser/parts/globalCompositeBar.ts index 8301e27c643..533fbd07508 100644 --- a/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -89,7 +89,7 @@ export class GlobalCompositeBar extends Disposable { contextMenuAlignmentOptions, (actions: IAction[]) => { actions.unshift(...[ - toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.PROFILE, StorageTarget.USER) }), + toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => setAccountsActionVisible(storageService, false) }), new Separator() ]); }); @@ -147,11 +147,11 @@ export class GlobalCompositeBar extends Disposable { } private get accountsVisibilityPreference(): boolean { - return this.storageService.getBoolean(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, StorageScope.PROFILE, true); + return isAccountsActionVisible(this.storageService); } private set accountsVisibilityPreference(value: boolean) { - this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, value, StorageScope.PROFILE, StorageTarget.USER); + setAccountsActionVisible(this.storageService, value); } } @@ -226,6 +226,9 @@ abstract class AbstractGlobalActivityActionViewItem extends CompositeBarActionVi // The rest of the activity bar uses context menu event for the context menu, so we match this this._register(addDisposableListener(this.container, EventType.CONTEXT_MENU, async (e: MouseEvent) => { + // Let the item decide on the context menu instead of the toolbar + e.stopPropagation(); + const disposables = new DisposableStore(); const actions = await this.resolveContextMenuActions(disposables); @@ -334,6 +337,11 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction })); this._register(this.authenticationService.onDidChangeSessions(async e => { + if (e.event.removed) { + for (const removed of e.event.removed) { + this.removeAccount(e.providerId, removed.account); + } + } for (const changed of [...(e.event.changed ?? []), ...(e.event.added ?? [])]) { try { await this.addOrUpdateAccount(e.providerId, changed.account); @@ -341,11 +349,6 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction this.logService.error(e); } } - if (e.event.removed) { - for (const removed of e.event.removed) { - this.removeAccount(e.providerId, removed.account); - } - } })); } @@ -631,20 +634,22 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, @ISecretStorageService secretStorageService: ISecretStorageService, + @IStorageService storageService: IStorageService, @ILogService logService: ILogService, @IActivityService activityService: IActivityService, @IInstantiationService instantiationService: IInstantiationService, @ICommandService commandService: ICommandService ) { - super(() => [], { - ...options, - colors: theme => ({ - badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND), - badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND), - }), - hoverOptions, - compact: true, - }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService); + super(() => simpleActivityContextMenuActions(storageService, true), + { + ...options, + colors: theme => ({ + badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND), + badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND), + }), + hoverOptions, + compact: true, + }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService); } } @@ -664,15 +669,40 @@ export class SimpleGlobalActivityActionViewItem extends GlobalActivityActionView @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IActivityService activityService: IActivityService, + @IStorageService storageService: IStorageService ) { - super(() => [], { - ...options, - colors: theme => ({ - badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND), - badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND), - }), - hoverOptions, - compact: true, - }, () => undefined, userDataProfileService, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService, instantiationService, activityService); + super(() => simpleActivityContextMenuActions(storageService, false), + { + ...options, + colors: theme => ({ + badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND), + badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND), + }), + hoverOptions, + compact: true, + }, () => undefined, userDataProfileService, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService, instantiationService, activityService); } } + +function simpleActivityContextMenuActions(storageService: IStorageService, isAccount: boolean): IAction[] { + const currentElementContextMenuActions: IAction[] = []; + if (isAccount) { + currentElementContextMenuActions.push( + toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => setAccountsActionVisible(storageService, false) }), + new Separator() + ); + } + return [ + ...currentElementContextMenuActions, + toAction({ id: 'toggle.hideAccounts', label: localize('accounts', "Accounts"), checked: isAccountsActionVisible(storageService), run: () => setAccountsActionVisible(storageService, !isAccountsActionVisible(storageService)) }), + toAction({ id: 'toggle.hideManage', label: localize('manage', "Manage"), checked: true, enabled: false, run: () => { throw new Error('"Manage" can not be hidden'); } }) + ]; +} + +export function isAccountsActionVisible(storageService: IStorageService): boolean { + return storageService.getBoolean(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, StorageScope.PROFILE, true); +} + +function setAccountsActionVisible(storageService: IStorageService, visible: boolean) { + storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, visible, StorageScope.PROFILE, StorageTarget.USER); +} diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 52baa5324f7..52f116455cf 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -73,13 +73,9 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { height: 35px; /* matches height of composite container */ - padding: 0 5px; + padding: 0 3px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon, -.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { - font-size: 18px; -} .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon), .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { @@ -237,6 +233,12 @@ text-align: center; } +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.compact-content .badge-content, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.compact-content .badge-content { + font-size: 8px; + padding: 0 3px; +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { mask-size: 11px; diff --git a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts new file mode 100644 index 00000000000..f36a4ddea7d --- /dev/null +++ b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IAccessibleViewService, AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IAccessibilitySignalService, AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { getNotificationFromContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { NotificationFocusedContext } from 'vs/workbench/common/contextkeys'; +import { INotificationViewItem } from 'vs/workbench/common/notifications'; + +export class NotificationAccessibleView implements IAccessibleViewImplentation { + readonly priority = 90; + readonly name = 'notifications'; + readonly when = NotificationFocusedContext; + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const accessibleViewService = accessor.get(IAccessibleViewService); + const listService = accessor.get(IListService); + const commandService = accessor.get(ICommandService); + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); + + function getProvider() { + const notification = getNotificationFromContext(listService); + if (!notification) { + return; + } + commandService.executeCommand('notifications.showList'); + let notificationIndex: number | undefined; + const list = listService.lastFocusedList; + if (list instanceof WorkbenchList) { + notificationIndex = list.indexOf(notification); + } + if (notificationIndex === undefined) { + return; + } + + function focusList(): void { + commandService.executeCommand('notifications.showList'); + if (list && notificationIndex !== undefined) { + list.domFocus(); + try { + list.setFocus([notificationIndex]); + } catch { } + } + } + + function getContentForNotification(): string | undefined { + const notification = getNotificationFromContext(listService); + const message = notification?.message.original.toString(); + if (!notification) { + return; + } + return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); + } + const content = getContentForNotification(); + if (!content) { + return; + } + notification.onDidClose(() => accessibleViewService.next()); + return new AccessibleContentProvider( + AccessibleViewProviderId.Notification, + { type: AccessibleViewType.View }, + () => content, + () => focusList(), + 'accessibility.verbosity.notification', + undefined, + getActionsFromNotification(notification, accessibilitySignalService), + () => { + if (!list) { + return; + } + focusList(); + list.focusNext(); + return getContentForNotification(); + }, + () => { + if (!list) { + return; + } + focusList(); + list.focusPrevious(); + return getContentForNotification(); + }, + ); + } + return getProvider(); + } +} + + +function getActionsFromNotification(notification: INotificationViewItem, accessibilitySignalService: IAccessibilitySignalService): IAction[] | undefined { + let actions = undefined; + if (notification.actions) { + actions = []; + if (notification.actions.primary) { + actions.push(...notification.actions.primary); + } + if (notification.actions.secondary) { + actions.push(...notification.actions.secondary); + } + } + if (actions) { + for (const action of actions) { + action.class = ThemeIcon.asClassName(Codicon.bell); + const initialAction = action.run; + action.run = () => { + initialAction(); + notification.close(); + }; + } + } + const manageExtension = actions?.find(a => a.label.includes('Manage Extension')); + if (manageExtension) { + manageExtension.class = ThemeIcon.asClassName(Codicon.gear); + } + if (actions) { + actions.push({ + id: 'clearNotification', label: localize('clearNotification', "Clear Notification"), tooltip: localize('clearNotification', "Clear Notification"), run: () => { + notification.close(); + accessibilitySignalService.playSignal(AccessibilitySignal.clear); + }, enabled: true, class: ThemeIcon.asClassName(Codicon.clearAll) + }); + } + return actions; +} + diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 84507d095dc..5151fdc18e1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -311,13 +311,13 @@ export function registerNotificationCommands(center: INotificationsCenterControl })); picker.canSelectMany = true; - picker.placeholder = localize('selectSources', "Select sources to enable notifications for"); - picker.selectedItems = picker.items.filter(item => (item as INotificationSourceFilter).filter === NotificationsFilter.OFF) as (IQuickPickItem & INotificationSourceFilter)[]; + picker.placeholder = localize('selectSources', "Select sources to enable all notifications from"); + picker.selectedItems = picker.items.filter(item => item.filter === NotificationsFilter.OFF); picker.show(); disposables.add(picker.onDidAccept(async () => { - for (const item of picker.items as (IQuickPickItem & INotificationSourceFilter)[]) { + for (const item of picker.items) { notificationService.setFilter({ id: item.id, label: item.label, @@ -354,7 +354,7 @@ type NotificationActionMetricsClassification = { id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run from a notification.' }; actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was run from a notification.' }; source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification where an action was run.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the notification where an action was run is silent or not.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification where an action was run is silent or not.' }; owner: 'bpasero'; comment: 'Tracks when actions are fired from notifcations and how they were fired.'; }; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts index 1a149ef7173..97d1d6a18c8 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts @@ -17,7 +17,7 @@ export interface NotificationMetrics { export type NotificationMetricsClassification = { id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the source of the notification.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the notification is silent or not.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification is silent or not.' }; source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification.' }; owner: 'bpasero'; comment: 'Helps us gain insights to what notifications are being shown, how many, and if they are silent or not.'; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 5d29de1726a..943136732c1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -55,7 +55,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast // Count for the number of notifications over 800ms... interval: 800, // ...and ensure we are not showing more than MAX_NOTIFICATIONS - limit: NotificationsToasts.MAX_NOTIFICATIONS + limit: this.MAX_NOTIFICATIONS }; private readonly _onDidChangeVisibility = this._register(new Emitter()); @@ -602,7 +602,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast if (visible) { notificationsToastsContainer.appendChild(toast.container); } else { - notificationsToastsContainer.removeChild(toast.container); + toast.container.remove(); } // Update visibility in model diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 246532ddd02..c1fda244291 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -29,8 +29,9 @@ import { Event } from 'vs/base/common/event'; import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -248,7 +249,7 @@ export class NotificationRenderer implements IListRenderer that.notificationService.setFilter({ ...source, filter: isSourceFiltered ? NotificationsFilter.OFF : NotificationsFilter.ERROR }) })); @@ -339,6 +340,7 @@ export class NotificationTemplateRenderer extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IHoverService private readonly hoverService: IHoverService, ) { super(); @@ -377,14 +379,14 @@ export class NotificationTemplateRenderer extends Disposable { this.renderSeverity(notification); // Message - const messageCustomHover = this.inputDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), this.template.message, '')); + const messageCustomHover = this.inputDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.template.message, '')); const messageOverflows = this.renderMessage(notification, messageCustomHover); // Secondary Actions this.renderSecondaryActions(notification, messageOverflows); // Source - const sourceCustomHover = this.inputDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), this.template.source, '')); + const sourceCustomHover = this.inputDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.template.source, '')); this.renderSource(notification, sourceCustomHover); // Buttons @@ -422,7 +424,7 @@ export class NotificationTemplateRenderer extends Disposable { this.template.icon.classList.add(...ThemeIcon.asClassNameArray(this.toSeverityIcon(notification.severity))); } - private renderMessage(notification: INotificationViewItem, customHover: ICustomHover): boolean { + private renderMessage(notification: INotificationViewItem, customHover: IManagedHover): boolean { clearNode(this.template.message); this.template.message.appendChild(NotificationMessageRenderer.render(notification.message, { callback: link => this.openerService.open(URI.parse(link), { allowCommands: true }), @@ -472,7 +474,7 @@ export class NotificationTemplateRenderer extends Disposable { actions.forEach(action => this.template.toolbar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) })); } - private renderSource(notification: INotificationViewItem, sourceCustomHover: ICustomHover): void { + private renderSource(notification: INotificationViewItem, sourceCustomHover: IManagedHover): void { if (notification.expanded && notification.source) { this.template.source.textContent = localize('notificationSource', "Source: {0}", notification.source); sourceCustomHover.update(notification.source); diff --git a/src/vs/workbench/browser/parts/paneCompositeBar.ts b/src/vs/workbench/browser/parts/paneCompositeBar.ts index 94dce01b958..999ec66db58 100644 --- a/src/vs/workbench/browser/parts/paneCompositeBar.ts +++ b/src/vs/workbench/browser/parts/paneCompositeBar.ts @@ -578,26 +578,26 @@ export class PaneCompositeBar extends Disposable { private storeCachedViewContainersState(cachedViewContainers: ICachedViewContainer[]): void { const pinnedViewContainers = this.getPinnedViewContainers(); - this.setPinnedViewContainers(cachedViewContainers.map(({ id, pinned, order }) => ({ + this.setPinnedViewContainers(cachedViewContainers.map(({ id, pinned, order }) => ({ id, pinned, - visible: pinnedViewContainers.find(({ id: pinnedId }) => pinnedId === id)?.visible, + visible: Boolean(pinnedViewContainers.find(({ id: pinnedId }) => pinnedId === id)?.visible), order - }))); + } satisfies IPinnedViewContainer))); - this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views, isBuiltin }) => ({ + this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views, isBuiltin }) => ({ id, iconUrl: URI.isUri(icon) ? icon : undefined, themeIcon: ThemeIcon.isThemeIcon(icon) ? icon : undefined, name, isBuiltin, views - }))); + } satisfies IPlaceholderViewContainer))); - this.setViewContainersWorkspaceState(cachedViewContainers.map(({ id, visible }) => ({ + this.setViewContainersWorkspaceState(cachedViewContainers.map(({ id, visible }) => ({ id, visible, - }))); + } satisfies IViewContainerWorkspaceState))); } private getPinnedViewContainers(): IPinnedViewContainer[] { diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 96bfffb6bc2..cf29cbed9f1 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -29,7 +29,6 @@ import { localize } from 'vs/nls'; import { CompositeDragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IPartOptions } from 'vs/workbench/browser/part'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { CompositeMenuActions } from 'vs/workbench/browser/actions'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -39,6 +38,8 @@ import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Composite } from 'vs/workbench/browser/composite'; import { ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { HiddenItemStrategy, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; export enum CompositeBarPosition { TOP, @@ -115,13 +116,13 @@ export abstract class AbstractPaneCompositePart extends CompositePart()); + private readonly paneCompositeBar = this._register(new MutableDisposable()); private compositeBarPosition: CompositeBarPosition | undefined = undefined; private emptyPaneMessageElement: HTMLElement | undefined; - private globalToolBar: ToolBar | undefined; + private globalToolBar: WorkbenchToolBar | undefined; private readonly globalActions: CompositeMenuActions; private blockOpening = false; @@ -141,6 +142,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart(registryId), @@ -312,13 +315,14 @@ export abstract class AbstractPaneCompositePart extends CompositePart this.actionViewItemProvider(action, options), orientation: ActionsOrientation.HORIZONTAL, getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment(), toggleMenuTitle: localize('moreActions', "More Actions..."), - hoverDelegate: this.toolbarHoverDelegate + hoverDelegate: this.toolbarHoverDelegate, + hiddenItemStrategy: HiddenItemStrategy.NoHide })); this.updateGlobalToolbarActions(); @@ -522,7 +526,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart true); + const menu = this.menuService.getMenuActions(ViewsSubMenu, scopedContextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); + createAndFillInActionBarActions(menu, { primary: viewsActions, secondary: [] }, () => true); disposables.dispose(); return viewsActions.length > 1 && viewsActions.some(a => a.enabled) ? new SubmenuAction('views', localize('views', "Views"), viewsActions) : undefined; } diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 40a5ee28faf..e1c147d8e88 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -17,6 +17,15 @@ border-top-width: 0; /* no border when main editor area is hiden */ } +.monaco-workbench .part.panel.top { + border-bottom-width: 1px; + border-bottom-style: solid; +} + +.monaco-workbench.nomaineditorarea .part.panel.top { + border-bottom-width: 0; /* no border when main editor area is hiden */ +} + .monaco-workbench .part.panel.right { border-left-width: 1px; border-left-style: solid; @@ -81,3 +90,11 @@ display: inline-block; transform: rotate(90deg); } + +/* Rotate icons when panel is on left */ +.monaco-workbench .part.basepanel.top .title-actions .codicon-split-horizontal::before, +.monaco-workbench .part.basepanel.top .global-actions .codicon-panel-maximize::before, +.monaco-workbench .part.basepanel.top .global-actions .codicon-panel-restore::before { + display: inline-block; + transform: rotate(180deg); +} diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 534db0283a7..49d0e197bf6 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -8,7 +8,7 @@ import { localize, localize2 } from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuId, MenuRegistry, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; +import { ActivityBarPosition, isHorizontal, IWorkbenchLayoutService, LayoutSettings, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/contextkeys'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; @@ -99,6 +99,7 @@ const PositionPanelActionId = { LEFT: 'workbench.action.positionPanelLeft', RIGHT: 'workbench.action.positionPanelRight', BOTTOM: 'workbench.action.positionPanelBottom', + TOP: 'workbench.action.positionPanelTop' }; const AlignPanelActionId = { @@ -136,6 +137,7 @@ function createAlignmentPanelActionConfig(id: string, title: ICommandActionTitle const PositionPanelActionConfigs: PanelActionConfig[] = [ + createPositionPanelActionConfig(PositionPanelActionId.TOP, localize2('positionPanelTop', "Move Panel To Top"), localize('positionPanelTopShort', "Top"), Position.TOP), createPositionPanelActionConfig(PositionPanelActionId.LEFT, localize2('positionPanelLeft', "Move Panel Left"), localize('positionPanelLeftShort', "Left"), Position.LEFT), createPositionPanelActionConfig(PositionPanelActionId.RIGHT, localize2('positionPanelRight', "Move Panel Right"), localize('positionPanelRightShort', "Right"), Position.RIGHT), createPositionPanelActionConfig(PositionPanelActionId.BOTTOM, localize2('positionPanelBottom', "Move Panel To Bottom"), localize('positionPanelBottomShort', "Bottom"), Position.BOTTOM), @@ -158,7 +160,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 4 }); -PositionPanelActionConfigs.forEach(positionPanelAction => { +PositionPanelActionConfigs.forEach((positionPanelAction, index) => { const { id, title, shortLabel, value, when } = positionPanelAction; registerAction2(class extends Action2 { @@ -182,7 +184,7 @@ PositionPanelActionConfigs.forEach(positionPanelAction => { title: shortLabel, toggled: when.negate() }, - order: 5 + order: 5 + index }); }); @@ -280,23 +282,23 @@ registerAction2(class extends Action2 { tooltip: localize('maximizePanel', "Maximize Panel Size"), category: Categories.View, f1: true, - icon: maximizeIcon, + icon: maximizeIcon, // This is being rotated in CSS depending on the panel position // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')), + precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), ContextKeyExpr.and(PanelPositionContext.notEqualsTo('bottom'), PanelPositionContext.notEqualsTo('top'))), toggled: { condition: PanelMaximizedContext, icon: restoreIcon, tooltip: localize('minimizePanel', "Restore Panel Size") }, menu: [{ id: MenuId.PanelTitle, group: 'navigation', order: 1, // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - when: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')) + when: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), ContextKeyExpr.and(PanelPositionContext.notEqualsTo('bottom'), PanelPositionContext.notEqualsTo('top'))) }] }); } run(accessor: ServicesAccessor) { const layoutService = accessor.get(IWorkbenchLayoutService); const notificationService = accessor.get(INotificationService); - if (layoutService.getPanelAlignment() !== 'center' && layoutService.getPanelPosition() === Position.BOTTOM) { + if (layoutService.getPanelAlignment() !== 'center' && isHorizontal(layoutService.getPanelPosition())) { notificationService.warn(localize('panelMaxNotSupported', "Maximizing the panel is only supported when it is center aligned.")); return; } @@ -351,10 +353,6 @@ registerAction2(class extends Action2 { group: 'navigation', order: 2, when: ContextKeyExpr.notEquals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP) - }, { - id: MenuId.AuxiliaryBarHeader, - group: 'navigation', - when: ContextKeyExpr.equals(`config.${LayoutSettings.ACTIVITY_BAR_LOCATION}`, ActivityBarPosition.TOP) }] }); } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index c85e46aedce..899d97b4cda 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -29,6 +29,7 @@ import { AbstractPaneCompositePart, CompositeBarPosition } from 'vs/workbench/br import { ICommandService } from 'vs/platform/commands/common/commands'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IPaneCompositeBarOptions } from 'vs/workbench/browser/parts/paneCompositeBar'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export class PanelPart extends AbstractPaneCompositePart { @@ -70,6 +71,7 @@ export class PanelPart extends AbstractPaneCompositePart { @IContextMenuService contextMenuService: IContextMenuService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IKeybindingService keybindingService: IKeybindingService, + @IHoverService hoverService: IHoverService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @@ -92,6 +94,7 @@ export class PanelPart extends AbstractPaneCompositePart { contextMenuService, layoutService, keybindingService, + hoverService, instantiationService, themeService, viewDescriptorService, @@ -109,6 +112,7 @@ export class PanelPart extends AbstractPaneCompositePart { const borderColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder) || ''; container.style.borderLeftColor = borderColor; container.style.borderRightColor = borderColor; + container.style.borderBottomColor = borderColor; const title = this.getTitleArea(); if (title) { @@ -146,14 +150,12 @@ export class PanelPart extends AbstractPaneCompositePart { } private fillExtraContextMenuActions(actions: IAction[]): void { - const panelPositionMenu = this.menuService.createMenu(MenuId.PanelPositionMenu, this.contextKeyService); - const panelAlignMenu = this.menuService.createMenu(MenuId.PanelAlignmentMenu, this.contextKeyService); + const panelPositionMenu = this.menuService.getMenuActions(MenuId.PanelPositionMenu, this.contextKeyService, { shouldForwardArgs: true }); + const panelAlignMenu = this.menuService.getMenuActions(MenuId.PanelAlignmentMenu, this.contextKeyService, { shouldForwardArgs: true }); const positionActions: IAction[] = []; const alignActions: IAction[] = []; - createAndFillInContextMenuActions(panelPositionMenu, { shouldForwardArgs: true }, { primary: [], secondary: positionActions }); - createAndFillInContextMenuActions(panelAlignMenu, { shouldForwardArgs: true }, { primary: [], secondary: alignActions }); - panelAlignMenu.dispose(); - panelPositionMenu.dispose(); + createAndFillInContextMenuActions(panelPositionMenu, { primary: [], secondary: positionActions }); + createAndFillInContextMenuActions(panelAlignMenu, { primary: [], secondary: alignActions }); actions.push(...[ new Separator(), @@ -165,10 +167,16 @@ export class PanelPart extends AbstractPaneCompositePart { override layout(width: number, height: number, top: number, left: number): void { let dimensions: Dimension; - if (this.layoutService.getPanelPosition() === Position.RIGHT) { - dimensions = new Dimension(width - 1, height); // Take into account the 1px border when layouting - } else { - dimensions = new Dimension(width, height); + switch (this.layoutService.getPanelPosition()) { + case Position.RIGHT: + dimensions = new Dimension(width - 1, height); // Take into account the 1px border when layouting + break; + case Position.TOP: + dimensions = new Dimension(width, height - 1); // Take into account the 1px border when layouting + break; + default: + dimensions = new Dimension(width, height); + break; } // Layout contents diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 0f2312e1f19..2c94078993b 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -16,6 +16,10 @@ margin-right: 4px; } +.monaco-workbench .part.sidebar > .title { + background-color: var(--vscode-sideBarTitle-background); +} + .monaco-workbench .part.sidebar > .title > .title-label h2 { text-transform: uppercase; } @@ -74,7 +78,7 @@ .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { position: absolute; - left: 6px; /* place icon in center */ + left: 5px; /* place icon in center */ } .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts b/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts index 8cafc0dbbcd..cd4811f83e6 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts @@ -37,7 +37,6 @@ export class FocusSideBarAction extends Action2 { // Show side bar if (!layoutService.isVisible(Parts.SIDEBAR_PART)) { layoutService.setPartHidden(false, Parts.SIDEBAR_PART); - return; } // Focus into active viewlet diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index a784caf2e0f..ac56632023d 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -31,6 +31,7 @@ import { Action2, IMenuService, registerAction2 } from 'vs/platform/actions/comm import { Separator } from 'vs/base/common/actions'; import { ToggleActivityBarVisibilityActionId } from 'vs/workbench/browser/actions/layoutActions'; import { localize2 } from 'vs/nls'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export class SidebarPart extends AbstractPaneCompositePart { @@ -42,6 +43,7 @@ export class SidebarPart extends AbstractPaneCompositePart { readonly maximumWidth: number = Number.POSITIVE_INFINITY; readonly minimumHeight: number = 0; readonly maximumHeight: number = Number.POSITIVE_INFINITY; + override get snap(): boolean { return true; } readonly priority: LayoutPriority = LayoutPriority.Low; @@ -60,7 +62,7 @@ export class SidebarPart extends AbstractPaneCompositePart { return Math.max(width, 300); } - private readonly acitivityBarPart: ActivitybarPart; + private readonly activityBarPart = this._register(this.instantiationService.createInstance(ActivitybarPart, this)); //#endregion @@ -70,6 +72,7 @@ export class SidebarPart extends AbstractPaneCompositePart { @IContextMenuService contextMenuService: IContextMenuService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IKeybindingService keybindingService: IKeybindingService, + @IHoverService hoverService: IHoverService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @@ -92,6 +95,7 @@ export class SidebarPart extends AbstractPaneCompositePart { contextMenuService, layoutService, keybindingService, + hoverService, instantiationService, themeService, viewDescriptorService, @@ -100,7 +104,6 @@ export class SidebarPart extends AbstractPaneCompositePart { menuService, ); - this.acitivityBarPart = this._register(instantiationService.createInstance(ActivitybarPart, this)); this.rememberActivityBarVisiblePosition(); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) { @@ -112,7 +115,7 @@ export class SidebarPart extends AbstractPaneCompositePart { } private onDidChangeActivityBarLocation(): void { - this.acitivityBarPart.hide(); + this.activityBarPart.hide(); this.updateCompositeBar(); @@ -122,7 +125,7 @@ export class SidebarPart extends AbstractPaneCompositePart { } if (this.shouldShowActivityBar()) { - this.acitivityBarPart.show(); + this.activityBarPart.show(); } this.rememberActivityBarVisiblePosition(); @@ -131,7 +134,6 @@ export class SidebarPart extends AbstractPaneCompositePart { override updateStyles(): void { super.updateStyles(); - // Part container const container = assertIsDefined(this.getContainer()); container.style.backgroundColor = this.getColor(SIDE_BAR_BACKGROUND) || ''; @@ -209,6 +211,7 @@ export class SidebarPart extends AbstractPaneCompositePart { if (this.shouldShowCompositeBar()) { return false; } + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.HIDDEN; } @@ -240,25 +243,28 @@ export class SidebarPart extends AbstractPaneCompositePart { } override getPinnedPaneCompositeIds(): string[] { - return this.shouldShowCompositeBar() ? super.getPinnedPaneCompositeIds() : this.acitivityBarPart.getPinnedPaneCompositeIds(); + return this.shouldShowCompositeBar() ? super.getPinnedPaneCompositeIds() : this.activityBarPart.getPinnedPaneCompositeIds(); } override getVisiblePaneCompositeIds(): string[] { - return this.shouldShowCompositeBar() ? super.getVisiblePaneCompositeIds() : this.acitivityBarPart.getVisiblePaneCompositeIds(); + return this.shouldShowCompositeBar() ? super.getVisiblePaneCompositeIds() : this.activityBarPart.getVisiblePaneCompositeIds(); } async focusActivityBar(): Promise { if (this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.HIDDEN) { await this.configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, this.getRememberedActivityBarVisiblePosition()); + this.onDidChangeActivityBarLocation(); } + if (this.shouldShowCompositeBar()) { - this.focusComositeBar(); + this.focusCompositeBar(); } else { if (!this.layoutService.isVisible(Parts.ACTIVITYBAR_PART)) { this.layoutService.setPartHidden(false, Parts.ACTIVITYBAR_PART); } - this.acitivityBarPart.show(true); + + this.activityBarPart.show(true); } } diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index d6897bf02d7..b30d2bee088 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -88,6 +88,11 @@ margin-left: 3px; } +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left.compact-right > .statusbar-item-label { + margin-right:0; + margin-left: 0; +} + .monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item { padding-left: 7px; /* Add padding to the most left status bar item */ } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index 76af9a42b7f..d458ce91e79 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -21,10 +21,11 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export class StatusbarEntryItem extends Disposable { @@ -41,7 +42,7 @@ export class StatusbarEntryItem extends Disposable { private readonly focusListener = this._register(new MutableDisposable()); private readonly focusOutListener = this._register(new MutableDisposable()); - private hover: ICustomHover | undefined = undefined; + private hover: IManagedHover | undefined = undefined; readonly labelContainer: HTMLElement; readonly beakContainer: HTMLElement; @@ -59,6 +60,7 @@ export class StatusbarEntryItem extends Disposable { entry: IStatusbarEntry, private readonly hoverDelegate: IHoverDelegate, @ICommandService private readonly commandService: ICommandService, + @IHoverService private readonly hoverService: IHoverService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IThemeService private readonly themeService: IThemeService @@ -120,7 +122,7 @@ export class StatusbarEntryItem extends Disposable { if (this.hover) { this.hover.update(hoverContents); } else { - this.hover = this._register(setupCustomHover(this.hoverDelegate, this.container, hoverContents)); + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents)); } if (entry.command !== ShowTooltipCommand /* prevents flicker on click */) { this.focusListener.value = addDisposableListener(this.labelContainer, EventType.FOCUS, e => { @@ -281,7 +283,7 @@ class StatusBarCodiconLabel extends SimpleIconLabel { private progressCodicon = renderIcon(syncing); private currentText = ''; - private currentShowProgress: boolean | 'syncing' | 'loading' = false; + private currentShowProgress: boolean | 'loading' | 'syncing' = false; constructor( private readonly container: HTMLElement @@ -289,10 +291,10 @@ class StatusBarCodiconLabel extends SimpleIconLabel { super(container); } - set showProgress(showProgress: boolean | 'syncing' | 'loading') { + set showProgress(showProgress: boolean | 'loading' | 'syncing') { if (this.currentShowProgress !== showProgress) { this.currentShowProgress = showProgress; - this.progressCodicon = renderIcon(showProgress === 'loading' ? spinningLoading : syncing); + this.progressCodicon = renderIcon(showProgress === 'syncing' ? syncing : spinningLoading); this.text = this.currentText; } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index ae4ef6a97e4..8c0ecf356a2 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -47,8 +47,7 @@ export class StatusbarViewModel extends Disposable { const hiddenRaw = this.storageService.get(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE); if (hiddenRaw) { try { - const hiddenArray: string[] = JSON.parse(hiddenRaw); - this.hidden = new Set(hiddenArray.filter(entry => !entry.startsWith('status.workspaceTrust.'))); // TODO@bpasero remove this migration eventually + this.hidden = new Set(JSON.parse(hiddenRaw)); } catch (error) { // ignore parsing errors } @@ -259,19 +258,34 @@ export class StatusbarViewModel extends Disposable { // - those with `priority: number` that can be compared // - those with `priority: string` that must be sorted // relative to another entry if possible - const mapEntryWithNumberedPriorityToIndex = new Map(); - const mapEntryWithRelativePriority = new Map(); + const mapEntryWithNumberedPriorityToIndex = new Map(); + const mapEntryWithRelativePriority = new Map>(); for (let i = 0; i < this._entries.length; i++) { const entry = this._entries[i]; if (typeof entry.priority.primary === 'number') { mapEntryWithNumberedPriorityToIndex.set(entry, i); } else { - let entries = mapEntryWithRelativePriority.get(entry.priority.primary.id); + const referenceEntryId = entry.priority.primary.id; + let entries = mapEntryWithRelativePriority.get(referenceEntryId); if (!entries) { - entries = []; - mapEntryWithRelativePriority.set(entry.priority.primary.id, entries); + + // It is possible that this entry references another entry + // that itself references an entry. In that case, we want + // to add it to the entries of the referenced entry. + + for (const relativeEntries of mapEntryWithRelativePriority.values()) { + if (relativeEntries.has(referenceEntryId)) { + entries = relativeEntries; + break; + } + } + + if (!entries) { + entries = new Map(); + mapEntryWithRelativePriority.set(referenceEntryId, entries); + } } - entries.push(entry); + entries.set(entry.id, entry); } } @@ -312,7 +326,8 @@ export class StatusbarViewModel extends Disposable { sortedEntries = []; for (const entry of sortedEntriesWithNumberedPriority) { - const relativeEntries = mapEntryWithRelativePriority.get(entry.id); + const relativeEntriesMap = mapEntryWithRelativePriority.get(entry.id); + const relativeEntries = relativeEntriesMap ? Array.from(relativeEntriesMap.values()) : undefined; // Fill relative entries to LEFT if (relativeEntries) { @@ -334,7 +349,7 @@ export class StatusbarViewModel extends Disposable { // Finally, just append all entries that reference another entry // that does not exist to the end of the list for (const [, entries] of mapEntryWithRelativePriority) { - sortedEntries.push(...entries); + sortedEntries.push(...entries.values()); } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index f938ea7b353..bac67111eae 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -433,7 +433,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { } // Figure out groups of entries with `compact` alignment - const compactEntryGroups = new Map>(); + const compactEntryGroups = new Map>(); for (const entry of mapIdToVisibleEntry.values()) { if ( isStatusbarEntryLocation(entry.priority.primary) && // entry references another entry as location @@ -448,11 +448,25 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { // Build a map of entries that are compact among each other let compactEntryGroup = compactEntryGroups.get(locationId); if (!compactEntryGroup) { - compactEntryGroup = new Set([entry, location]); - compactEntryGroups.set(locationId, compactEntryGroup); - } else { - compactEntryGroup.add(entry); + + // It is possible that this entry references another entry + // that itself references an entry. In that case, we want + // to add it to the entries of the referenced entry. + + for (const group of compactEntryGroups.values()) { + if (group.has(locationId)) { + compactEntryGroup = group; + break; + } + } + + if (!compactEntryGroup) { + compactEntryGroup = new Map(); + compactEntryGroups.set(locationId, compactEntryGroup); + } } + compactEntryGroup.set(entry.id, entry); + compactEntryGroup.set(location.id, location); // Adjust CSS classes to move compact items closer together if (entry.priority.primary.alignment === StatusbarAlignment.LEFT) { @@ -465,7 +479,6 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { } } - // Install mouse listeners to update hover feedback for // all compact entries that belong to each other const statusBarItemHoverBackground = this.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); @@ -473,7 +486,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.compactEntriesDisposable.value = new DisposableStore(); if (statusBarItemHoverBackground && statusBarItemCompactHoverBackground && !isHighContrast(this.theme.type)) { for (const [, compactEntryGroup] of compactEntryGroups) { - for (const compactEntry of compactEntryGroup) { + for (const compactEntry of compactEntryGroup.values()) { if (!compactEntry.hasCommand) { continue; // only show hover feedback when we have a command } diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 54eded7aab3..88d5435936b 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -7,7 +7,6 @@ import { isActiveDocument, reset } from 'vs/base/browser/dom'; import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; @@ -22,6 +21,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { WindowTitle } from 'vs/workbench/browser/parts/titlebar/windowTitle'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export class CommandCenterControl { @@ -82,6 +82,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { private readonly _submenu: SubmenuItemAction, private readonly _windowTitle: WindowTitle, options: IBaseActionViewItemOptions, + @IHoverService private readonly _hoverService: IHoverService, @IKeybindingService private _keybindingService: IKeybindingService, @IInstantiationService private _instaService: IInstantiationService, @IEditorGroupsService private _editorGroupService: IEditorGroupsService, @@ -95,7 +96,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { container.classList.add('command-center-center'); container.classList.toggle('multiple', (this._submenu.actions.length > 1)); - const hover = this._store.add(setupCustomHover(this._hoverDelegate, container, this.getTooltip())); + const hover = this._store.add(this._hoverService.setupManagedHover(this._hoverDelegate, container, this.getTooltip())); // update label & tooltip when window title changes this._store.add(this._windowTitle.onDidChange(() => { @@ -156,7 +157,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { labelElement.innerText = label; reset(container, searchIcon, labelElement); - const hover = this._store.add(setupCustomHover(that._hoverDelegate, container, this.getTooltip())); + const hover = this._store.add(that._hoverService.setupManagedHover(that._hoverDelegate, container, this.getTooltip())); // update label & tooltip when window title changes this._store.add(that._windowTitle.onDidChange(() => { diff --git a/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css b/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css index b41a6f925da..923c221df25 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css +++ b/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css @@ -22,6 +22,10 @@ color: var(--vscode-activityBar-foreground); } +.monaco-workbench .activitybar .menubar.compact > .menubar-menu-button:focus { + background-color: var(--vscode-menubar-selectionBackground); +} + .monaco-workbench .menubar.inactive:not(.compact) > .menubar-menu-button, .monaco-workbench .menubar.inactive:not(.compact) > .menubar-menu-button .toolbar-toggle-more { color: var(--vscode-titleBar-inactiveForeground); diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 5b8cf1a3a7e..23fe9a5b1b4 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -143,7 +143,7 @@ export abstract class MenubarControl extends Disposable { protected topLevelTitles: { [menu: string]: string } = {}; - protected mainMenuDisposables: DisposableStore; + protected readonly mainMenuDisposables: DisposableStore; protected recentlyOpened: IRecentlyOpened = { files: [], workspaces: [] }; @@ -564,7 +564,7 @@ export class CustomMenubarControl extends MenubarControl { return result; } - private reinstallDisposables = this._register(new DisposableStore()); + private readonly reinstallDisposables = this._register(new DisposableStore()); private setupCustomMenubar(firstTime: boolean): void { // If there is no container, we cannot setup the menubar if (!this.container) { diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index 632bf55e2e2..16832936d7c 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from 'vs/nls'; +import { ILocalizedString, localize, localize2 } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -19,11 +19,12 @@ import { CustomTitleBarVisibility, TitleBarSetting, TitlebarStyle } from 'vs/pla class ToggleConfigAction extends Action2 { - constructor(private readonly section: string, title: string, order: number, mainWindowOnly: boolean) { + constructor(private readonly section: string, title: string, description: string | ILocalizedString | undefined, order: number, mainWindowOnly: boolean) { const when = mainWindowOnly ? IsAuxiliaryWindowFocusedContext.toNegated() : ContextKeyExpr.true(); super({ id: `toggle.${section}`, title, + metadata: description ? { description } : undefined, toggled: ContextKeyExpr.equals(`config.${section}`, true), menu: [ { @@ -51,13 +52,13 @@ class ToggleConfigAction extends Action2 { registerAction2(class ToggleCommandCenter extends ToggleConfigAction { constructor() { - super(LayoutSettings.COMMAND_CENTER, localize('toggle.commandCenter', 'Command Center'), 1, false); + super(LayoutSettings.COMMAND_CENTER, localize('toggle.commandCenter', 'Command Center'), localize('toggle.commandCenterDescription', "Toggle visibility of the Command Center in title bar"), 1, false); } }); registerAction2(class ToggleLayoutControl extends ToggleConfigAction { constructor() { - super('workbench.layoutControl.enabled', localize('toggle.layout', 'Layout Controls'), 2, true); + super('workbench.layoutControl.enabled', localize('toggle.layout', 'Layout Controls'), localize('toggle.layoutDescription', "Toggle visibility of the Layout Controls in title bar"), 2, true); } }); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 6439d953ac1..f610512e84f 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -7,8 +7,8 @@ import 'vs/css!./media/titlebarpart'; import { localize, localize2 } from 'vs/nls'; import { MultiWindowParts, Part } from 'vs/workbench/browser/part'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; -import { getZoomFactor, isWCOEnabled } from 'vs/base/browser/browser'; -import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, TitlebarStyle, hasCustomTitlebar, hasNativeTitlebar } from 'vs/platform/window/common/window'; +import { getWCOBoundingRect, getZoomFactor, isWCOEnabled } from 'vs/base/browser/browser'; +import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, TitlebarStyle, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from 'vs/platform/window/common/window'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; @@ -19,11 +19,11 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { isMacintosh, isWindows, isLinux, isWeb, isNative, platformLocale } from 'vs/base/common/platform'; import { Color } from 'vs/base/common/color'; -import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset, getWindow, getWindowId, isAncestor, getActiveDocument } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset, getWindow, getWindowId, isAncestor, getActiveDocument, isHTMLElement } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService, ActivityBarPosition, LayoutSettings, EditorActionsLocation, EditorTabsMode } from 'vs/workbench/services/layout/browser/layoutService'; import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -36,7 +36,7 @@ import { CommandCenterControl } from 'vs/workbench/browser/parts/titlebar/comman import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; -import { SimpleAccountActivityActionViewItem, SimpleGlobalActivityActionViewItem } from 'vs/workbench/browser/parts/globalCompositeBar'; +import { AccountsActivityActionViewItem, isAccountsActionVisible, SimpleAccountActivityActionViewItem, SimpleGlobalActivityActionViewItem } from 'vs/workbench/browser/parts/globalCompositeBar'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IEditorGroupsContainer, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ActionRunner, IAction } from 'vs/base/common/actions'; @@ -200,7 +200,11 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { readonly maximumWidth: number = Number.POSITIVE_INFINITY; get minimumHeight(): number { - const value = this.isCommandCenterVisible || (isWeb && isWCOEnabled()) ? 35 : 30; + const wcoEnabled = isWeb && isWCOEnabled(); + let value = this.isCommandCenterVisible || wcoEnabled ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30; + if (wcoEnabled) { + value = Math.max(value, getWCOBoundingRect()?.height ?? 0); + } return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1); } @@ -235,13 +239,14 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { private lastLayoutDimensions: Dimension | undefined; private actionToolBar!: WorkbenchToolBar; - private actionToolBarDisposable = this._register(new DisposableStore()); - private editorActionsChangeDisposable = this._register(new DisposableStore()); + private readonly actionToolBarDisposable = this._register(new DisposableStore()); + private readonly editorActionsChangeDisposable = this._register(new DisposableStore()); private actionToolBarElement!: HTMLElement; private layoutToolbarMenu: IMenu | undefined; private readonly editorToolbarMenuDisposables = this._register(new DisposableStore()); private readonly layoutToolbarMenuDisposables = this._register(new DisposableStore()); + private readonly activityToolbarDisposables = this._register(new DisposableStore()); private readonly hoverDelegate: IHoverDelegate; @@ -265,7 +270,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { @IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @@ -473,7 +478,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { EventHelper.stop(e); let targetMenu: MenuId; - if (isMacintosh && e.target instanceof HTMLElement && isAncestor(e.target, this.title)) { + if (isMacintosh && isHTMLElement(e.target) && isAncestor(e.target, this.title)) { targetMenu = MenuId.TitleBarTitleContext; } else { targetMenu = MenuId.TitleBarContext; @@ -609,7 +614,9 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { // --- Activity Actions if (this.activityActionsEnabled) { - actions.primary.push(ACCOUNTS_ACTIVITY_TILE_ACTION); + if (isAccountsActionVisible(this.storageService)) { + actions.primary.push(ACCOUNTS_ACTIVITY_TILE_ACTION); + } actions.primary.push(GLOBAL_ACTIVITY_TITLE_ACTION); } @@ -631,7 +638,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner); } else { this.actionToolBar.actionRunner = new ActionRunner(); - this.actionToolBar.context = {}; + this.actionToolBar.context = undefined; this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner); } @@ -650,6 +657,13 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } } + if (update.activityActions) { + this.activityToolbarDisposables.clear(); + if (this.activityActionsEnabled) { + this.activityToolbarDisposables.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, this._store)(() => updateToolBarActions())); + } + } + updateToolBarActions(); } diff --git a/src/vs/workbench/browser/parts/views/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts index 62058fd74fc..f428de9bffe 100644 --- a/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/src/vs/workbench/browser/parts/views/checkbox.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; +import type { IHoverService } from 'vs/platform/hover/browser/hover'; import { defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ITreeItem, ITreeItemCheckboxState } from 'vs/workbench/common/views'; @@ -27,14 +28,19 @@ export class TreeItemCheckbox extends Disposable { public toggle: Toggle | undefined; private checkboxContainer: HTMLDivElement; public isDisposed = false; - private hover: ICustomHover | undefined; + private hover: IManagedHover | undefined; public static readonly checkboxClass = 'custom-view-tree-node-item-checkbox'; private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; - constructor(container: HTMLElement, private checkboxStateHandler: CheckboxStateHandler, private readonly hoverDelegate: IHoverDelegate) { + constructor( + container: HTMLElement, + private checkboxStateHandler: CheckboxStateHandler, + private readonly hoverDelegate: IHoverDelegate, + private readonly hoverService: IHoverService + ) { super(); this.checkboxContainer = container; } @@ -81,7 +87,7 @@ export class TreeItemCheckbox extends Disposable { private setHover(checkbox: ITreeItemCheckboxState) { if (this.toggle) { if (!this.hover) { - this.hover = this._register(setupCustomHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox))); + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox))); } else { this.hover.update(checkbox.tooltip); } @@ -116,7 +122,7 @@ export class TreeItemCheckbox extends Disposable { private removeCheckbox() { const children = this.checkboxContainer.children; for (const child of children) { - this.checkboxContainer.removeChild(child); + child.remove(); } } } diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 9e343669291..2ba481c4d93 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -46,10 +46,23 @@ padding-left: 24px; } -.monaco-workbench .tree-explorer-viewlet-tree-view .message a { +.monaco-workbench .tree-explorer-viewlet-tree-view .message p > a { color: var(--vscode-textLink-foreground); } +.monaco-workbench .tree-explorer-viewlet-tree-view .message .rendered-message { + width: 100%; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .message .button-container { + width: 100%; + max-width: 300px; + margin: auto; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .message .button-container:not(:last-child) { + padding-bottom: 8px; +} .monaco-workbench .tree-explorer-viewlet-tree-view .message.hide { display: none; } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 661638fa561..5b82b9f971b 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -9,19 +9,18 @@ import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ITooltipMarkdownString } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData, ListViewTargetSector } from 'vs/base/browser/ui/list/listView'; import { IAsyncDataSource, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, ITreeNode, ITreeRenderer, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; -import { ActionRunner, IAction } from 'vs/base/common/actions'; +import { ActionRunner, IAction, Separator } from 'vs/base/common/actions'; import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, isMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { Schemas } from 'vs/base/common/network'; @@ -34,10 +33,10 @@ import 'vs/css!./media/views'; import { VSDataTransfer } from 'vs/base/common/dataTransfer'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { Action2, IMenu, IMenuService, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyChangeEvent, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -56,14 +55,12 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { getLocationBasedViewColors, IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { Extensions, ITreeItem, ITreeItemLabel, ITreeView, ITreeViewDataProvider, ITreeViewDescriptor, ITreeViewDragAndDropController, IViewBadge, IViewDescriptorService, IViewsRegistry, ResolvableTreeItem, TreeCommand, TreeItemCollapsibleState, TreeViewItemHandleArg, TreeViewPaneHandleArg, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; -import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { CodeDataTransfers, LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { CheckboxStateHandler, TreeItemCheckbox } from 'vs/workbench/browser/parts/views/checkbox'; @@ -73,6 +70,12 @@ import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUti import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import type { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import { parseLinkedText } from 'vs/base/common/linkedText'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { Command } from 'vs/editor/common/languages'; export class TreeViewPane extends ViewPane { @@ -91,9 +94,11 @@ export class TreeViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, - @INotificationService notificationService: INotificationService + @INotificationService notificationService: INotificationService, + @IHoverService hoverService: IHoverService, + @IAccessibleViewInformationService accessibleViewService: IAccessibleViewInformationService, ) { - super({ ...(options as IViewPaneOptions), titleMenuId: MenuId.ViewTitle, donotForwardArgs: false }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super({ ...(options as IViewPaneOptions), titleMenuId: MenuId.ViewTitle, donotForwardArgs: false }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService, accessibleViewService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); @@ -171,20 +176,29 @@ class Root implements ITreeItem { children: ITreeItem[] | undefined = undefined; } -function isTreeCommandEnabled(treeCommand: TreeCommand, contextKeyService: IContextKeyService): boolean { - const command = CommandsRegistry.getCommand(treeCommand.originalId ? treeCommand.originalId : treeCommand.id); +function commandPreconditions(commandId: string): ContextKeyExpression | undefined { + const command = CommandsRegistry.getCommand(commandId); if (command) { const commandAction = MenuRegistry.getCommand(command.id); - const precondition = commandAction && commandAction.precondition; - if (precondition) { - return contextKeyService.contextMatchesRules(precondition); - } + return commandAction && commandAction.precondition; } + return undefined; +} + +function isTreeCommandEnabled(treeCommand: TreeCommand | Command, contextKeyService: IContextKeyService): boolean { + const commandId: string = (treeCommand as TreeCommand).originalId ? (treeCommand as TreeCommand).originalId! : treeCommand.id; + const precondition = commandPreconditions(commandId); + if (precondition) { + return contextKeyService.contextMatchesRules(precondition); + } + return true; } -function isRenderedMessageValue(messageValue: string | IMarkdownRenderResult | undefined): messageValue is IMarkdownRenderResult { - return !!messageValue && typeof messageValue !== 'string' && 'element' in messageValue && 'dispose' in messageValue; +interface RenderedMessage { element: HTMLElement; disposables: DisposableStore } + +function isRenderedMessageValue(messageValue: string | RenderedMessage | undefined): messageValue is RenderedMessage { + return !!messageValue && typeof messageValue !== 'string' && 'element' in messageValue && 'disposables' in messageValue; } const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); @@ -209,7 +223,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private focused: boolean = false; private domNode!: HTMLElement; private treeContainer: HTMLElement | undefined; - private _messageValue: string | { element: HTMLElement; dispose: () => void } | undefined; + private _messageValue: string | { element: HTMLElement; disposables: DisposableStore } | undefined; private _canSelectMany: boolean = false; private _manuallyManageCheckboxes: boolean = false; private messageElement: HTMLElement | undefined; @@ -268,7 +282,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { @IHoverService private readonly hoverService: IHoverService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IActivityService private readonly activityService: IActivityService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); this.root = new Root(); @@ -305,7 +320,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { })); this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { if (views.some(v => v.id === this.id)) { - this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND } }); + this.tree?.updateOptions({ overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles }); } })); this.registerActions(); @@ -338,6 +353,9 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { set dataProvider(dataProvider: ITreeViewDataProvider | undefined) { if (dataProvider) { + if (this.visible) { + this.activate(); + } const self = this; this._dataProvider = new class implements ITreeViewDataProvider { private _isEmpty: boolean = true; @@ -388,6 +406,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this.refresh(); } else { this._dataProvider = undefined; + this.treeDisposables.clear(); + this.activated = false; this.updateMessage(); } @@ -445,6 +465,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { priority: 50 }; this._activity.value = this.activityService.showViewActivity(this.id, activity); + } else { + this._activity.clear(); } } @@ -596,6 +618,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } } + protected activated: boolean = false; protected abstract activate(): void; focus(reveal: boolean = true, revealItem?: ITreeItem): void { @@ -631,19 +654,21 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this._register(focusTracker.onDidBlur(() => this.focused = false)); } + private readonly treeDisposables: DisposableStore = this._register(new DisposableStore()); protected createTree() { + this.treeDisposables.clear(); const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); - const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); - this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); + const treeMenus = this.treeDisposables.add(this.instantiationService.createInstance(TreeMenus, this.id)); + this.treeLabels = this.treeDisposables.add(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); const aligner = new Aligner(this.themeService); - const checkboxStateHandler = this._register(new CheckboxStateHandler()); + const checkboxStateHandler = this.treeDisposables.add(new CheckboxStateHandler()); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler, () => this.manuallyManageCheckboxes); - this._register(renderer.onDidChangeCheckboxState(e => this._onDidChangeCheckboxState.fire(e))); + this.treeDisposables.add(renderer.onDidChangeCheckboxState(e => this._onDidChangeCheckboxState.fire(e))); const widgetAriaLabel = this._title; - this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer!, new TreeViewDelegate(), [renderer], + this.tree = this.treeDisposables.add(this.instantiationService.createInstance(Tree, this.id, this.treeContainer!, new TreeViewDelegate(), [renderer], dataSource, { identityProvider: new TreeViewIdentityProvider(), accessibilityProvider: { @@ -690,10 +715,12 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { }, multipleSelectionSupport: this.canSelectMany, dnd: this.treeViewDnd, - overrideStyles: { - listBackground: this.viewLocation === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND - } + overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles }) as WorkbenchAsyncDataTree); + + this.treeDisposables.add(renderer.onDidChangeMenuContext(e => e.forEach(e => this.tree?.rerender(e)))); + + this.treeDisposables.add(this.tree); treeMenus.setContextKeyService(this.tree.contextKeyService); aligner.tree = this.tree; const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()); @@ -702,21 +729,21 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this.tree.contextKeyService.createKey(this.id, true); const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService); customTreeKey.set(true); - this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); + this.treeDisposables.add(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); - this._register(this.tree.onDidChangeSelection(e => { + this.treeDisposables.add(this.tree.onDidChangeSelection(e => { this.lastSelection = e.elements; this.lastActive = this.tree?.getFocus()[0] ?? this.lastActive; this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive }); })); - this._register(this.tree.onDidChangeFocus(e => { + this.treeDisposables.add(this.tree.onDidChangeFocus(e => { if (e.elements.length && (e.elements[0] !== this.lastActive)) { this.lastActive = e.elements[0]; this.lastSelection = this.tree?.getSelection() ?? this.lastSelection; this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive }); } })); - this._register(this.tree.onDidChangeCollapseState(e => { + this.treeDisposables.add(this.tree.onDidChangeCollapseState(e => { if (!e.node.element) { return; } @@ -730,7 +757,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { })); this.tree.setInput(this.root).then(() => this.updateContentAreas()); - this._register(this.tree.onDidOpen(async (e) => { + this.treeDisposables.add(this.tree.onDidOpen(async (e) => { if (!e.browserEvent) { return; } @@ -756,7 +783,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } })); - this._register(treeMenus.onDidChange((changed) => { + this.treeDisposables.add(treeMenus.onDidChange((changed) => { if (this.tree?.hasNode(changed)) { this.tree?.rerender(changed); } @@ -786,7 +813,12 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { event.stopPropagation(); this.tree!.setFocus([node]); - const actions = treeMenus.getResourceContextActions(node); + let selected = this.canSelectMany ? this.getSelection() : []; + if (!selected.find(item => item.handle === node.handle)) { + selected = [node]; + } + + const actions = treeMenus.getResourceContextActions(selected); if (!actions.length) { return; } @@ -809,7 +841,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } }, - getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), + getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg), actionRunner }); @@ -826,14 +858,73 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this.updateContentAreas(); } + private processMessage(message: IMarkdownString, disposables: DisposableStore): HTMLElement { + const lines = message.value.split('\n'); + const result: (IMarkdownRenderResult | HTMLElement)[] = []; + let hasFoundButton = false; + for (const line of lines) { + const linkedText = parseLinkedText(line); + + if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { + const node = linkedText.nodes[0]; + const buttonContainer = document.createElement('div'); + buttonContainer.classList.add('button-container'); + const button = new Button(buttonContainer, { title: node.title, secondary: hasFoundButton, supportIcons: true, ...defaultButtonStyles }); + button.label = node.label; + button.onDidClick(_ => { + this.openerService.open(node.href, { allowCommands: true }); + }, null, disposables); + + const href = URI.parse(node.href); + if (href.scheme === Schemas.command) { + const preConditions = commandPreconditions(href.path); + if (preConditions) { + button.enabled = this.contextKeyService.contextMatchesRules(preConditions); + disposables.add(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(new Set(preConditions.keys()))) { + button.enabled = this.contextKeyService.contextMatchesRules(preConditions); + } + })); + } + } + + disposables.add(button); + hasFoundButton = true; + result.push(buttonContainer); + } else { + hasFoundButton = false; + const rendered = this.markdownRenderer!.render(new MarkdownString(line, { isTrusted: message.isTrusted, supportThemeIcons: message.supportThemeIcons, supportHtml: message.supportHtml })); + result.push(rendered.element); + disposables.add(rendered); + } + } + + const container = document.createElement('div'); + container.classList.add('rendered-message'); + for (const child of result) { + if (DOM.isHTMLElement(child)) { + container.appendChild(child); + } else { + container.appendChild(child.element); + } + } + return container; + } + private showMessage(message: string | IMarkdownString): void { if (isRenderedMessageValue(this._messageValue)) { - this._messageValue.dispose(); + this._messageValue.disposables.dispose(); } if (isMarkdownString(message) && !this.markdownRenderer) { this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); } - this._messageValue = isMarkdownString(message) ? this.markdownRenderer!.render(message) : message; + if (isMarkdownString(message)) { + const disposables = new DisposableStore(); + const renderedMessage = this.processMessage(message, disposables); + this._messageValue = { element: renderedMessage, disposables }; + } else { + this._messageValue = message; + } if (!this.messageElement) { return; } @@ -1069,7 +1160,6 @@ class TreeDataSource implements IAsyncDataSource { } interface ITreeExplorerTemplateData { - readonly elementDisposable: DisposableStore; readonly container: HTMLElement; readonly resourceLabel: IResourceLabel; readonly icon: HTMLElement; @@ -1085,6 +1175,9 @@ class TreeRenderer extends Disposable implements ITreeRenderer = this._register(new Emitter()); readonly onDidChangeCheckboxState: Event = this._onDidChangeCheckboxState.event; + private _onDidChangeMenuContext: Emitter = this._register(new Emitter()); + readonly onDidChangeMenuContext: Event = this._onDidChangeMenuContext.event; + private _actionRunner: MultipleSelectionActionRunner | undefined; private _hoverDelegate: IHoverDelegate; private _hasCheckbox: boolean = false; @@ -1101,8 +1194,8 @@ class TreeRenderer extends Disposable implements ITreeRenderer { this.updateCheckboxes(items); })); + this._register(this.contextKeyService.onDidChangeContext(e => this.onDidChangeContext(e))); } get templateId(): string { @@ -1133,10 +1227,10 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; + templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg; - const menuActions = this.menus.getResourceActions(node, templateData.elementDisposable); - templateData.actionBar.push(menuActions.actions, { icon: true, label: false }); + const menuActions = this.menus.getResourceActions([node]); + templateData.actionBar.push(menuActions, { icon: true, label: false }); if (this._actionRunner) { templateData.actionBar.actionRunner = this._actionRunner; } this.setAlignment(templateData.container, node); - this.treeViewsService.addRenderedTreeItemElement(node.handle, templateData.container); // remember rendered element, an element can be rendered multiple times const renderedItems = this._renderedElements.get(element.element.handle) ?? []; @@ -1286,7 +1379,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData): void { - templateData.elementDisposable.clear(); - const itemRenders = this._renderedElements.get(resource.element.handle) ?? []; const renderedIndex = itemRenders.findIndex(renderedItem => templateData === renderedItem.rendered); @@ -1419,8 +1524,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer[], newActions: IAction[]) { + const newActionsSet: Set = new Set(newActions.map(a => a.id)); + for (const group of groups) { + const actions = group.keys(); + for (const action of actions) { + if (!newActionsSet.has(action)) { + group.delete(action); + } + } + } + } + + private buildMenu(groups: Map[]): IAction[] { + const result: IAction[] = []; + for (const group of groups) { + if (group.size > 0) { + if (result.length) { + result.push(new Separator()); + } + result.push(...group.values()); + } + } + return result; + } + + private createGroups(actions: IAction[]): Map[] { + const groups: Map[] = []; + let group: Map = new Map(); + for (const action of actions) { + if (action instanceof Separator) { + groups.push(group); + group = new Map(); + } else { + group.set(action.id, action); + } + } + groups.push(group); + return groups; + } + + public getElementOverlayContexts(element: ITreeItem): Map { + return new Map([ + ['view', this.id], + ['viewItem', element.contextValue] + ]); + } + + public getEntireMenuContexts(): ReadonlySet { + return this.menuService.getMenuContexts(this.getMenuId()); + } + + public getMenuId(): MenuId { + return MenuId.ViewItemContext; + } + + private getActions(menuId: MenuId, elements: ITreeItem[]): { primary: IAction[]; secondary: IAction[] } { if (!this.contextKeyService) { return { primary: [], secondary: [] }; } - const contextKeyService = this.contextKeyService.createOverlay([ - ['view', this.id], - ['viewItem', element.contextValue] - ]); + let primaryGroups: Map[] = []; + let secondaryGroups: Map[] = []; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const contextKeyService = this.contextKeyService.createOverlay(this.getElementOverlayContexts(element)); - const menu = this.menuService.createMenu(menuId, contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary, menu }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, 'inline'); - if (listen) { - listen.add(menu.onDidChange(() => this._onDidChange.fire(element))); - listen.add(menu); - } else { - menu.dispose(); + const menuData = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true }); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + createAndFillInContextMenuActions(menuData, result, 'inline'); + if (i === 0) { + primaryGroups = this.createGroups(result.primary); + secondaryGroups = this.createGroups(result.secondary); + } else { + this.filterNonUniversalActions(primaryGroups, result.primary); + this.filterNonUniversalActions(secondaryGroups, result.secondary); + } } - return result; + + return { primary: this.buildMenu(primaryGroups), secondary: this.buildMenu(secondaryGroups) }; } dispose() { @@ -1565,8 +1733,6 @@ class TreeMenus implements IDisposable { export class CustomTreeView extends AbstractTreeView { - private activated: boolean = false; - constructor( id: string, title: string, @@ -1586,8 +1752,9 @@ export class CustomTreeView extends AbstractTreeView { @IActivityService activityService: IActivityService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService logService: ILogService, + @IOpenerService openerService: IOpenerService ) { - super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService); + super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService, openerService); } protected activate() { @@ -1619,8 +1786,6 @@ export class CustomTreeView extends AbstractTreeView { export class TreeView extends AbstractTreeView { - private activated: boolean = false; - protected activate() { if (!this.activated) { this.createTree(); diff --git a/src/vs/workbench/browser/parts/views/viewFilter.ts b/src/vs/workbench/browser/parts/views/viewFilter.ts index b6285e45c71..724a67b931a 100644 --- a/src/vs/workbench/browser/parts/views/viewFilter.ts +++ b/src/vs/workbench/browser/parts/views/viewFilter.ts @@ -81,6 +81,7 @@ export class FilterWidget extends Widget { private moreFiltersActionViewItem: MoreFiltersActionViewItem | undefined; private isMoreFiltersChecked: boolean = false; + private lastWidth?: number; private focusTracker: DOM.IFocusTracker; public get onDidFocus() { return this.focusTracker.onDidFocus; } @@ -147,6 +148,13 @@ export class FilterWidget extends Widget { this.element.parentElement?.classList.toggle('grow', width > 700); this.element.classList.toggle('small', width < 400); this.adjustInputBox(); + this.lastWidth = width; + } + + relayout() { + if (this.lastWidth) { + this.layout(this.lastWidth); + } } checkMoreFilters(checked: boolean): void { @@ -222,6 +230,8 @@ export class FilterWidget extends Widget { if (event.equals(KeyCode.Space) || event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) + || event.equals(KeyCode.Home) + || event.equals(KeyCode.End) ) { event.stopPropagation(); } diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 294c69b1448..ea8047a243d 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -7,7 +7,6 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { asCssVariable, foreground } from 'vs/platform/theme/common/colorRegistry'; -import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault } from 'vs/base/browser/dom'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; @@ -23,7 +22,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { Extensions as ViewContainerExtensions, IView, IViewDescriptorService, ViewContainerLocation, IViewsRegistry, IViewContentDescriptor, defaultViewIcon, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined, PartialExcept } from 'vs/base/common/types'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { MenuId, Action2, IAction2Options, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -47,8 +46,13 @@ import { FilterWidget, IFilterWidgetOptions } from 'vs/workbench/browser/parts/v import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { PANEL_BACKGROUND, PANEL_STICKY_SCROLL_BACKGROUND, PANEL_STICKY_SCROLL_BORDER, PANEL_STICKY_SCROLL_SHADOW, SIDE_BAR_BACKGROUND, SIDE_BAR_STICKY_SCROLL_BACKGROUND, SIDE_BAR_STICKY_SCROLL_BORDER, SIDE_BAR_STICKY_SCROLL_SHADOW } from 'vs/workbench/common/theme'; +import { IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; export enum ViewPaneShowActions { /** Show the actions when the view is hovered. This is the default behavior. */ @@ -120,9 +124,10 @@ class ViewWelcomeController { @IOpenerService protected openerService: IOpenerService, @ITelemetryService protected telemetryService: ITelemetryService, @IContextKeyService private contextKeyService: IContextKeyService, + @ILifecycleService lifecycleService: ILifecycleService ) { - this.delegate.onDidChangeViewWelcomeState(this.onDidChangeViewWelcomeState, this, this.disposables); - this.onDidChangeViewWelcomeState(); + this.disposables.add(Event.runAndSubscribe(this.delegate.onDidChangeViewWelcomeState, () => this.onDidChangeViewWelcomeState())); + this.disposables.add(lifecycleService.onWillShutdown(() => this.dispose())); // Fixes https://github.com/microsoft/vscode/issues/208878 } layout(height: number, width: number) { @@ -349,11 +354,11 @@ export abstract class ViewPane extends Pane implements IView { private readonly showActions: ViewPaneShowActions; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; - private titleContainerHover?: ICustomHover; + private titleContainerHover?: IManagedHover; private titleDescriptionContainer?: HTMLElement; - private titleDescriptionContainerHover?: ICustomHover; + private titleDescriptionContainerHover?: IManagedHover; private iconContainer?: HTMLElement; - private iconContainerHover?: ICustomHover; + private iconContainerHover?: IManagedHover; protected twistiesContainer?: HTMLElement; private viewWelcomeController!: ViewWelcomeController; @@ -370,6 +375,8 @@ export abstract class ViewPane extends Pane implements IView { @IOpenerService protected openerService: IOpenerService, @IThemeService protected themeService: IThemeService, @ITelemetryService protected telemetryService: ITelemetryService, + @IHoverService protected readonly hoverService: IHoverService, + protected readonly accessibleViewInformationService?: IAccessibleViewInformationService ) { super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); @@ -384,7 +391,8 @@ export abstract class ViewPane extends Pane implements IView { const viewLocationKey = this.scopedContextKeyService.createKey('viewLocation', ViewContainerLocationToString(viewDescriptorService.getViewLocationById(this.id)!)); this._register(Event.filter(viewDescriptorService.onDidChangeLocation, e => e.views.some(view => view.id === this.id))(() => viewLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewLocationById(this.id)!)))); - this.menuActions = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])).createInstance(CompositeMenuActions, options.titleMenuId ?? MenuId.ViewTitle, MenuId.ViewTitleContext, { shouldForwardArgs: !options.donotForwardArgs, renderShortTitle: true })); + const childInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + this.menuActions = this._register(childInstantiationService.createInstance(CompositeMenuActions, options.titleMenuId ?? MenuId.ViewTitle, MenuId.ViewTitleContext, { shouldForwardArgs: !options.donotForwardArgs, renderShortTitle: true })); this._register(this.menuActions.onDidChange(() => this.updateActions())); } @@ -533,14 +541,24 @@ export abstract class ViewPane extends Pane implements IView { const calculatedTitle = this.calculateTitle(title); this.titleContainer = append(container, $('h3.title', {}, calculatedTitle)); - this.titleContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle)); + this.titleContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle)); if (this._titleDescription) { this.setTitleDescription(this._titleDescription); } - this.iconContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.iconContainer, calculatedTitle)); - this.iconContainer.setAttribute('aria-label', calculatedTitle); + this.iconContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.iconContainer, calculatedTitle)); + this.iconContainer.setAttribute('aria-label', this._getAriaLabel(calculatedTitle)); + } + + private _getAriaLabel(title: string): string { + const viewHasAccessibilityHelpContent = this.viewDescriptorService.getViewDescriptorById(this.id)?.accessibilityHelpContent; + const accessibleViewHasShownForView = this.accessibleViewInformationService?.hasShownAccessibleView(this.id); + if (!viewHasAccessibilityHelpContent || accessibleViewHasShownForView) { + return title; + } + + return nls.localize('viewAccessibilityHelp', 'Use Alt+F1 for accessibility help {0}', title); } protected updateTitle(title: string): void { @@ -552,7 +570,7 @@ export abstract class ViewPane extends Pane implements IView { if (this.iconContainer) { this.iconContainerHover?.update(calculatedTitle); - this.iconContainer.setAttribute('aria-label', calculatedTitle); + this.iconContainer.setAttribute('aria-label', this._getAriaLabel(calculatedTitle)); } this._title = title; @@ -566,7 +584,7 @@ export abstract class ViewPane extends Pane implements IView { } else if (description && this.titleContainer) { this.titleDescriptionContainer = after(this.titleContainer, $('span.description', {}, description)); - this.titleDescriptionContainerHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.titleDescriptionContainer, description)); + this.titleDescriptionContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.titleDescriptionContainer, description)); } } @@ -591,7 +609,7 @@ export abstract class ViewPane extends Pane implements IView { } protected renderBody(container: HTMLElement): void { - this.viewWelcomeController = this._register(new ViewWelcomeController(container, this, this.instantiationService, this.openerService, this.telemetryService, this.contextKeyService)); + this.viewWelcomeController = this._register(this.instantiationService.createInstance(ViewWelcomeController, container, this)); } protected layoutBody(height: number, width: number): void { @@ -625,16 +643,8 @@ export abstract class ViewPane extends Pane implements IView { return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; } - protected getBackgroundColor(): string { - switch (this.viewDescriptorService.getViewLocationById(this.id)) { - case ViewContainerLocation.Panel: - return PANEL_BACKGROUND; - case ViewContainerLocation.Sidebar: - case ViewContainerLocation.AuxiliaryBar: - return SIDE_BAR_BACKGROUND; - } - - return SIDE_BAR_BACKGROUND; + protected getLocationBasedColors(): IViewPaneLocationColors { + return getLocationBasedViewColors(this.viewDescriptorService.getViewLocationById(this.id)); } focus(): void { @@ -679,7 +689,9 @@ export abstract class ViewPane extends Pane implements IView { override get trapsArrowNavigation(): boolean { return true; } override render(container: HTMLElement): void { container.classList.add('viewpane-filter-container'); - append(container, that.getFilterWidget()!.element); + const filter = that.getFilterWidget()!; + append(container, filter.element); + filter.relayout(); } }; } @@ -732,9 +744,12 @@ export abstract class FilterViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IHoverService hoverService: IHoverService, + accessibleViewService?: IAccessibleViewInformationService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.filterWidget = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])).createInstance(FilterWidget, options.filterOptions)); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService, accessibleViewService); + const childInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + this.filterWidget = this._register(childInstantiationService.createInstance(FilterWidget, options.filterOptions)); } override getFilterWidget(): FilterWidget { @@ -776,6 +791,42 @@ export abstract class FilterViewPane extends ViewPane { } +export interface IViewPaneLocationColors { + background: string; + listOverrideStyles: PartialExcept; +} + +export function getLocationBasedViewColors(location: ViewContainerLocation | null): IViewPaneLocationColors { + let background, stickyScrollBackground, stickyScrollBorder, stickyScrollShadow; + + switch (location) { + case ViewContainerLocation.Panel: + background = PANEL_BACKGROUND; + stickyScrollBackground = PANEL_STICKY_SCROLL_BACKGROUND; + stickyScrollBorder = PANEL_STICKY_SCROLL_BORDER; + stickyScrollShadow = PANEL_STICKY_SCROLL_SHADOW; + break; + + case ViewContainerLocation.Sidebar: + case ViewContainerLocation.AuxiliaryBar: + default: + background = SIDE_BAR_BACKGROUND; + stickyScrollBackground = SIDE_BAR_STICKY_SCROLL_BACKGROUND; + stickyScrollBorder = SIDE_BAR_STICKY_SCROLL_BORDER; + stickyScrollShadow = SIDE_BAR_STICKY_SCROLL_SHADOW; + } + + return { + background, + listOverrideStyles: { + listBackground: background, + treeStickyScrollBackground: stickyScrollBackground, + treeStickyScrollBorder: stickyScrollBorder, + treeStickyScrollShadow: stickyScrollShadow + } + }; +} + export abstract class ViewAction extends Action2 { override readonly desc: Readonly & { viewId: string }; constructor(desc: Readonly & { viewId: string }) { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 67b7f28268f..01df5c12a69 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -39,15 +39,15 @@ import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerMo import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchLayoutService, LayoutSettings, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { isHorizontal, IWorkbenchLayoutService, LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; export const ViewsSubMenu = new MenuId('Views'); -MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { submenu: ViewsSubMenu, title: nls.localize('views', "Views"), order: 1, -}); +} satisfies ISubmenuItem); export interface IViewPaneContainerOptions extends IPaneViewOptions { mergeViewWithContainerWhenSingleView: boolean; @@ -112,7 +112,7 @@ class ViewPaneDropOverlay extends Themable { this.paneElement.appendChild(this.container); this.paneElement.classList.add('dragged-over'); this._register(toDisposable(() => { - this.paneElement.removeChild(this.container); + this.container.remove(); this.paneElement.classList.remove('dragged-over'); })); @@ -625,8 +625,9 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { case ViewContainerLocation.Sidebar: case ViewContainerLocation.AuxiliaryBar: return Orientation.VERTICAL; - case ViewContainerLocation.Panel: - return this.layoutService.getPanelPosition() === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + case ViewContainerLocation.Panel: { + return isHorizontal(this.layoutService.getPanelPosition()) ? Orientation.HORIZONTAL : Orientation.VERTICAL; + } } return Orientation.VERTICAL; diff --git a/src/vs/workbench/browser/quickaccess.ts b/src/vs/workbench/browser/quickaccess.ts index aec2065963d..bf732b89be1 100644 --- a/src/vs/workbench/browser/quickaccess.ts +++ b/src/vs/workbench/browser/quickaccess.ts @@ -82,7 +82,6 @@ export class PickerEditorState extends Disposable { state: getIEditor(activeEditorPane.getControl())?.saveViewState() ?? undefined, }; } - } /** diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 196edf88796..697e6742183 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -103,9 +103,23 @@ export interface IWorkbench { * `ExtensionTerminalOptions` in the extension API. */ createTerminal(options: IEmbedderTerminalOptions): Promise; + + /** + * Show an information message to users. Optionally provide an array of items which will be presented as + * clickable buttons. + * + * @param message The message to show. + * @param items A set of items that will be rendered as actions in the message. + * @returns A thenable that resolves to the selected item or `undefined` when being dismissed. + */ + showInformationMessage(message: string, ...items: T[]): Promise; }; workspace: { + /** + * Resolves once the remote authority has been resolved. + */ + didResolveRemoteAuthority(): Promise; /** * Forwards a port. If the current embedder implements a tunnelFactory then that will be used to make the tunnel. diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index 5fab020f5de..f1ff86dddbb 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -150,10 +150,23 @@ export namespace window { const workbench = await workbenchPromise.p; workbench.window.createTerminal(options); } + + export async function showInformationMessage(message: string, ...items: T[]): Promise { + const workbench = await workbenchPromise.p; + return await workbench.window.showInformationMessage(message, ...items); + } } export namespace workspace { + /** + * {@linkcode IWorkbench.workspace IWorkbench.workspace.didResolveRemoteAuthority} + */ + export async function didResolveRemoteAuthority() { + const workbench = await workbenchPromise.p; + await workbench.workspace.didResolveRemoteAuthority(); + } + /** * {@linkcode IWorkbench.workspace IWorkbench.workspace.openTunnel} */ diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 6250a90d6c1..1ea687ba1c6 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -76,7 +76,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { BrowserUserDataProfilesService } from 'vs/platform/userDataProfile/browser/userDataProfile'; -import { timeout } from 'vs/base/common/async'; +import { DeferredPromise, timeout } from 'vs/base/common/async'; import { windowLogId } from 'vs/workbench/services/log/common/logConstants'; import { LogService } from 'vs/platform/log/common/logService'; import { IRemoteSocketFactoryService, RemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; @@ -95,6 +95,7 @@ import { IEncryptionService } from 'vs/platform/encryption/common/encryptionServ import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; import { TunnelSource } from 'vs/workbench/services/remote/common/tunnelModel'; import { mainWindow } from 'vs/base/browser/window'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; export class BrowserMain extends Disposable { @@ -156,6 +157,23 @@ export class BrowserMain extends Disposable { const remoteExplorerService = accessor.get(IRemoteExplorerService); const labelService = accessor.get(ILabelService); const embedderTerminalService = accessor.get(IEmbedderTerminalService); + const remoteAuthorityResolverService = accessor.get(IRemoteAuthorityResolverService); + const notificationService = accessor.get(INotificationService); + + async function showMessage(severity: Severity, message: string, ...items: T[]): Promise { + const choice = new DeferredPromise(); + const handle = notificationService.prompt(severity, message, items.map(item => ({ + label: item, + run: () => choice.complete(item) + }))); + const disposable = handle.onDidClose(() => { + choice.complete(undefined); + disposable.dispose(); + }); + const result = await choice.p; + handle.close(); + return result; + } let logger: DelayedLogChannel | undefined = undefined; @@ -188,8 +206,16 @@ export class BrowserMain extends Disposable { window: { withProgress: (options, task) => progressService.withProgress(options, task), createTerminal: async (options) => embedderTerminalService.createTerminal(options), + showInformationMessage: (message, ...items) => showMessage(Severity.Info, message, ...items), }, workspace: { + didResolveRemoteAuthority: async () => { + if (!this.configuration.remoteAuthority) { + return; + } + + await remoteAuthorityResolverService.resolveAuthority(this.configuration.remoteAuthority); + }, openTunnel: async tunnelOptions => { const tunnel = assertIsDefined(await remoteExplorerService.forward({ remote: tunnelOptions.remoteAddress, @@ -222,7 +248,7 @@ export class BrowserMain extends Disposable { } }, shutdown: () => lifecycleService.shutdown() - }; + } satisfies IWorkbench; }); } diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index e964af6543e..84c49ea97c8 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -133,9 +133,19 @@ export abstract class BaseWindow extends Disposable { continue; // skip over hidden windows (but never over main window) } - const handle = (window as any).vscodeOriginalSetTimeout.apply(this, [handlerFn, timeout, ...args]); + // we track didClear in case the browser does not properly clear the timeout + // this can happen for timeouts on unfocused windows + let didClear = false; + + const handle = (window as any).vscodeOriginalSetTimeout.apply(this, [(...args: unknown[]) => { + if (didClear) { + return; + } + handlerFn(...args); + }, timeout, ...args]); const timeoutDisposable = toDisposable(() => { + didClear = true; (window as any).vscodeOriginalClearTimeout(handle); timeoutDisposables.delete(timeoutDisposable); }); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 8f88583cf4f..d83ac066c6e 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -6,8 +6,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform'; -import { ConfigurationMigrationWorkbenchContribution, DynamicWorkbenchSecurityConfiguration, IConfigurationMigrationRegistry, workbenchConfigurationNodeBase, Extensions, ConfigurationKeyValuePairs, problemsConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { isMacintosh, isWindows, isLinux, isWeb, isNative } from 'vs/base/common/platform'; +import { ConfigurationMigrationWorkbenchContribution, DynamicWorkbenchSecurityConfiguration, IConfigurationMigrationRegistry, workbenchConfigurationNodeBase, Extensions, ConfigurationKeyValuePairs, problemsConfigurationNodeBase, windowConfigurationNodeBase, DynamicWindowConfiguration } from 'vs/workbench/common/configuration'; import { isStandalone } from 'vs/base/browser/browser'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { ActivityBarPosition, EditorActionsLocation, EditorTabsMode, LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; @@ -29,6 +29,12 @@ const registry = Registry.as(ConfigurationExtensions.Con registry.registerConfiguration({ ...workbenchConfigurationNodeBase, 'properties': { + 'workbench.externalBrowser': { + type: 'string', + markdownDescription: localize('browser', "Configure the browser to use for opening http or https links externally. This can either be the name of the browser (`edge`, `chrome`, `firefox`) or an absolute path to the browser's executable. Will use the system default if not set."), + included: isNative, + restricted: true + }, 'workbench.editor.titleScrollbarSizing': { type: 'string', enum: ['default', 'large'], @@ -61,6 +67,11 @@ const registry = Registry.as(ConfigurationExtensions.Con 'markdownDescription': localize('editorActionsLocation', "Controls where the editor actions are shown."), 'default': 'default' }, + 'workbench.editor.alwaysShowEditorActions': { + 'type': 'boolean', + 'markdownDescription': localize('alwaysShowEditorActions', "Controls whether to always show the editor actions, even when the editor group is not active."), + 'default': false + }, 'workbench.editor.wrapTabs': { 'type': 'boolean', 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'wrapTabs' }, "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when {0} is not set to '{1}'.", '`#workbench.editor.showTabs#`', '`multiple`'), @@ -94,21 +105,22 @@ const registry = Registry.as(ConfigurationExtensions.Con [CustomEditorLabelService.SETTING_ID_PATTERNS]: { 'type': 'object', 'markdownDescription': (() => { - let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:"); + let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. The relative path must include the WORKSPACE_FOLDER (e.g `WORKSPACE_FOLDER/src/**.tsx` or `*/src/**.tsx`). Absolute patterns must start with a `/`. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:"); customEditorLabelDescription += '\n- ' + [ - localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `root/folder/file.txt -> folder`)."), - localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=1: root/folder/file.txt -> root`)."), - localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `root/folder/file.txt -> file`)."), - localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `root/folder/file.txt -> txt`)."), + localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `WORKSPACE_FOLDER/folder/file.txt -> folder`)."), + localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absolute path, otherwise it corresponds to the workspace folder."), + localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> file`)."), + localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> txt`)."), + localize('workbench.editor.label.nthextname', "`${extname(N)}`: the nth extension of the file separated by '.' (e.g. `N=2: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext1`). Extension can be picked from the start of the extension by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext2`)."), ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations - customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `root/static/folder/file.html` as `file - folder (html)`."); + customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `WORKSPACE_FOLDER/static/folder/file.html` as `file - folder (html)`."); return customEditorLabelDescription; })(), additionalProperties: { - type: 'string', - markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern mtches. May include the variables ${dirname}, ${filename} and ${extname}."), + type: ['string', 'null'], + markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern matches. May include the variables ${dirname}, ${filename} and ${extname}."), minLength: 1, pattern: '.*[a-zA-Z0-9].*' }, @@ -504,9 +516,9 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.panel.defaultLocation': { 'type': 'string', - 'enum': ['left', 'bottom', 'right'], + 'enum': ['left', 'bottom', 'top', 'right'], 'default': 'bottom', - 'description': localize('panelDefaultLocation', "Controls the default location of the panel (Terminal, Debug Console, Output, Problems) in a new workspace. It can either show at the bottom, right, or left of the editor area."), + 'description': localize('panelDefaultLocation', "Controls the default location of the panel (Terminal, Debug Console, Output, Problems) in a new workspace. It can either show at the bottom, top, right, or left of the editor area."), }, 'workbench.panel.opensMaximized': { 'type': 'string', @@ -528,22 +540,22 @@ const registry = Registry.as(ConfigurationExtensions.Con 'type': 'string', 'enum': ['default', 'top', 'bottom', 'hidden'], 'default': 'default', - 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar. It can either show to the `default` or `top` / `bottom` of the Primary and Secondary Side Bar or `hidden`."), + 'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Secondary Side Bars."), 'enumDescriptions': [ - localize('workbench.activityBar.location.default', "Show the Activity Bar of the Primary Side Bar on the side."), - localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bar."), - localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bar."), - localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bar.") + localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Secondary Side Bar."), + localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bars."), + localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bars."), + localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bars.") ], }, 'workbench.activityBar.iconClickBehavior': { 'type': 'string', 'enum': ['toggle', 'focus'], 'default': 'toggle', - 'description': localize('activityBarIconClickBehavior', "Controls the behavior of clicking an activity bar icon in the workbench."), + 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'activityBarIconClickBehavior' }, "Controls the behavior of clicking an Activity Bar icon in the workbench. This value is ignored when {0} is not set to {1}.", '`#workbench.activityBar.location#`', '`default`'), 'enumDescriptions': [ - localize('workbench.activityBar.iconClickBehavior.toggle', "Hide the side bar if the clicked item is already visible."), - localize('workbench.activityBar.iconClickBehavior.focus', "Focus side bar if the clicked item is already visible.") + localize('workbench.activityBar.iconClickBehavior.toggle', "Hide the Primary Side Bar if the clicked item is already visible."), + localize('workbench.activityBar.iconClickBehavior.focus', "Focus the Primary Side Bar if the clicked item is already visible.") ] }, 'workbench.view.alwaysShowHeaderActions': { @@ -648,10 +660,7 @@ const registry = Registry.as(ConfigurationExtensions.Con ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations registry.registerConfiguration({ - 'id': 'window', - 'order': 8, - 'title': localize('windowConfigurationTitle', "Window"), - 'type': 'object', + ...windowConfigurationNodeBase, 'properties': { 'window.title': { 'type': 'string', @@ -757,6 +766,9 @@ const registry = Registry.as(ConfigurationExtensions.Con } }); + // Dynamic Window Configuration + registerWorkbenchContribution2(DynamicWindowConfiguration.ID, DynamicWindowConfiguration, WorkbenchPhase.Eventually); + // Problems registry.registerConfiguration({ ...problemsConfigurationNodeBase, diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index c7302a6be67..b0688133537 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -5,7 +5,7 @@ import 'vs/workbench/browser/style'; import { localize } from 'vs/nls'; -import { addDisposableListener, runWhenWindowIdle } from 'vs/base/browser/dom'; +import { runWhenWindowIdle } from 'vs/base/browser/dom'; import { Event, Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; import { RunOnceScheduler, timeout } from 'vs/base/common/async'; import { isFirefox, isSafari, isChrome } from 'vs/base/browser/browser'; @@ -43,8 +43,13 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mainWindow } from 'vs/base/browser/window'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { setBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; +import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler'; +import { setProgressAcccessibilitySignalScheduler } from 'vs/base/browser/ui/progressbar/progressAccessibilitySignal'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { NotificationAccessibleView } from 'vs/workbench/browser/parts/notifications/notificationAccessibleView'; export interface IWorkbenchOptions { @@ -79,14 +84,16 @@ export class Workbench extends Layout { private registerErrorHandler(logService: ILogService): void { // Listen on unhandled rejection events - this._register(addDisposableListener(mainWindow, 'unhandledrejection', event => { + // Note: intentionally not registered as disposable to handle + // errors that can occur during shutdown phase. + mainWindow.addEventListener('unhandledrejection', (event) => { // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent onUnexpectedError(event.reason); // Prevent the printing of this event to the console event.preventDefault(); - })); + }); // Install handler for unexpected errors setUnexpectedErrorHandler(error => this.handleUnexpectedError(error, logService)); @@ -151,12 +158,14 @@ export class Workbench extends Layout { const storageService = accessor.get(IStorageService); const configurationService = accessor.get(IConfigurationService); const hostService = accessor.get(IHostService); + const hoverService = accessor.get(IHoverService); const dialogService = accessor.get(IDialogService); const notificationService = accessor.get(INotificationService) as NotificationService; // Default Hover Delegate must be registered before creating any workbench/layout components // as these possibly will use the default hover delegate setHoverDelegateFactory((placement, enableInstantHover) => instantiationService.createInstance(WorkbenchHoverDelegate, placement, enableInstantHover, {})); + setBaseLayerHoverDelegate(hoverService); // Layout this.initLayout(accessor); @@ -322,8 +331,9 @@ export class Workbench extends Layout { private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void { - // ARIA + // ARIA & Signals setARIAContainer(this.mainContainer); + setProgressAcccessibilitySignalScheduler((msDelayTime: number, msLoopTime?: number) => instantiationService.createInstance(AccessibilityProgressSignalScheduler, msDelayTime, msLoopTime)); // State specific classes const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac'; @@ -408,6 +418,9 @@ export class Workbench extends Layout { // Register Commands registerNotificationCommands(notificationsCenter, notificationsToasts, notificationService.model); + // Register notification accessible view + AccessibleViewRegistry.register(new NotificationAccessibleView()); + // Register with Layout this.registerNotifications({ onDidChangeNotificationsVisibility: Event.map(Event.any(notificationsToasts.onDidChangeVisibility, notificationsCenter.onDidChangeVisibility), () => notificationsToasts.isVisible || notificationsCenter.isVisible) diff --git a/src/vs/workbench/common/configuration.ts b/src/vs/workbench/common/configuration.ts index 4e15bd458be..20bd1258ca4 100644 --- a/src/vs/workbench/common/configuration.ts +++ b/src/vs/workbench/common/configuration.ts @@ -16,6 +16,7 @@ import { OperatingSystem, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { equals } from 'vs/base/common/objects'; import { DeferredPromise } from 'vs/base/common/async'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export const applicationConfigurationNodeBase = Object.freeze({ 'id': 'application', @@ -46,6 +47,13 @@ export const problemsConfigurationNodeBase = Object.freeze({ 'order': 101 }); +export const windowConfigurationNodeBase = Object.freeze({ + 'id': 'window', + 'order': 8, + 'title': localize('windowConfigurationTitle', "Window"), + 'type': 'object', +}); + export const Extensions = { ConfigurationMigration: 'base.contributions.configuration.migration' }; @@ -225,3 +233,72 @@ export class DynamicWorkbenchSecurityConfiguration extends Disposable implements }); } } + +export const CONFIG_NEW_WINDOW_PROFILE = 'window.newWindowProfile'; + +export class DynamicWindowConfiguration extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.dynamicWindowConfiguration'; + + private configurationNode: IConfigurationNode | undefined; + private newWindowProfile: IUserDataProfile | undefined; + + constructor( + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + this.registerNewWindowProfileConfiguration(); + this._register(this.userDataProfilesService.onDidChangeProfiles((e) => this.registerNewWindowProfileConfiguration())); + + this.setNewWindowProfile(); + this.checkAndResetNewWindowProfileConfig(); + + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.source !== ConfigurationTarget.DEFAULT && e.affectsConfiguration(CONFIG_NEW_WINDOW_PROFILE)) { + this.setNewWindowProfile(); + } + })); + this._register(this.userDataProfilesService.onDidChangeProfiles(() => this.checkAndResetNewWindowProfileConfig())); + } + + private registerNewWindowProfileConfiguration(): void { + const registry = Registry.as(ConfigurationExtensions.Configuration); + const configurationNode: IConfigurationNode = { + ...windowConfigurationNodeBase, + 'properties': { + [CONFIG_NEW_WINDOW_PROFILE]: { + 'type': ['string', 'null'], + 'default': null, + 'enum': [...this.userDataProfilesService.profiles.map(profile => profile.name), null], + 'enumItemLabels': [...this.userDataProfilesService.profiles.map(p => ''), localize('active window', "Active Window")], + 'description': localize('newWindowProfile', "Specifies the profile to use when opening a new window. If a profile name is provided, the new window will use that profile. If no profile name is provided, the new window will use the profile of the active window or the Default profile if no active window exists."), + 'scope': ConfigurationScope.APPLICATION, + } + } + }; + if (this.configurationNode) { + registry.updateConfigurations({ add: [configurationNode], remove: [this.configurationNode] }); + } else { + registry.registerConfiguration(configurationNode); + } + this.configurationNode = configurationNode; + } + + private setNewWindowProfile(): void { + const newWindowProfileName = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE); + this.newWindowProfile = newWindowProfileName ? this.userDataProfilesService.profiles.find(profile => profile.name === newWindowProfileName) : undefined; + } + + private checkAndResetNewWindowProfileConfig(): void { + const newWindowProfileName = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE); + if (!newWindowProfileName) { + return; + } + const profile = this.newWindowProfile ? this.userDataProfilesService.profiles.find(profile => profile.id === this.newWindowProfile!.id) : undefined; + if (newWindowProfileName === profile?.name) { + return; + } + this.configurationService.updateValue(CONFIG_NEW_WINDOW_PROFILE, profile?.name); + } +} diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 612f006a88c..d02f9e0e89d 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -72,6 +72,9 @@ export const ActiveEditorGroupLastContext = new RawContextKey('activeEd export const ActiveEditorGroupLockedContext = new RawContextKey('activeEditorGroupLocked', false, localize('activeEditorGroupLocked', "Whether the active editor group is locked")); export const MultipleEditorGroupsContext = new RawContextKey('multipleEditorGroups', false, localize('multipleEditorGroups', "Whether there are multiple editor groups opened")); export const SingleEditorGroupsContext = MultipleEditorGroupsContext.toNegated(); +export const MultipleEditorsSelectedInGroupContext = new RawContextKey('multipleEditorsSelectedInGroup', false, localize('multipleEditorsSelectedInGroup', "Whether multiple editors have been selected in an editor group")); +export const TwoEditorsSelectedInGroupContext = new RawContextKey('twoEditorsSelectedInGroup', false, localize('twoEditorsSelectedInGroup', "Whether exactly two editors have been selected in an editor group")); +export const SelectedEditorsInGroupFileOrUntitledResourceContextKey = new RawContextKey('SelectedEditorsInGroupFileOrUntitledResourceContextKey', true, localize('SelectedEditorsInGroupFileOrUntitledResourceContextKey', "Whether all selected editors in a group have a file or untitled resource associated")); // Editor Part Context Keys export const EditorPartMultipleEditorGroupsContext = new RawContextKey('editorPartMultipleEditorGroups', false, localize('editorPartMultipleEditorGroups', "Whether there are multiple editor groups opened in an editor part")); @@ -292,13 +295,12 @@ export function applyAvailableEditorIds(contextKey: IContextKey, editor: } const editorResource = editor.resource; - const editors = editorResource ? editorResolverService.getEditors(editorResource).map(editor => editor.id) : []; - if (editorResource?.scheme === Schemas.untitled && editor.editorId !== DEFAULT_EDITOR_ASSOCIATION.id) { // Non text editor untitled files cannot be easily serialized between extensions // so instead we disable this context key to prevent common commands that act on the active editor contextKey.set(''); } else { + const editors = editorResource ? editorResolverService.getEditors(editorResource).map(editor => editor.id) : []; contextKey.set(editors.join(',')); } } diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index aaf1452c25a..f7e23fae0a2 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -11,7 +11,7 @@ import { mark } from 'vs/base/common/performance'; import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { getOrSet } from 'vs/base/common/map'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, isDisposable } from 'vs/base/common/lifecycle'; import { IEditorPaneService } from 'vs/workbench/services/editor/common/editorPaneService'; /** @@ -156,6 +156,7 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb private readonly contributionsById = new Map(); private readonly instancesById = new Map(); + private readonly instanceDisposables = this._register(new DisposableStore()); private readonly timingsByPhase = new Map>(); get timings() { return this.timingsByPhase; } @@ -249,6 +250,11 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb const environmentService = this.environmentService = accessor.get(IEnvironmentService); const editorPaneService = this.editorPaneService = accessor.get(IEditorPaneService); + // Dispose contributions on shutdown + this._register(lifecycleService.onDidShutdown(() => { + this.instanceDisposables.clear(); + })); + // Instantiate contributions by phase when they are ready for (const phase of [LifecyclePhase.Starting, LifecyclePhase.Ready, LifecyclePhase.Restored, LifecyclePhase.Eventually]) { this.instantiateByPhase(instantiationService, lifecycleService, logService, environmentService, phase); @@ -377,6 +383,9 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb this.instancesById.set(contribution.id, instance); this.contributionsById.delete(contribution.id); } + if (isDisposable(instance)) { + this.instanceDisposables.add(instance); + } } catch (error) { logService.error(`Unable to create workbench contribution '${contribution.id ?? contribution.ctor.name}'.`, error); } finally { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index bd2c42d8510..d3d6adb4ff5 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -547,7 +547,7 @@ export interface IResourceMultiDiffEditorInput extends IBaseUntypedEditorInput { * The list of resources to compare. * If not set, the resources are dynamically derived from the {@link multiDiffSource}. */ - readonly resources?: IResourceDiffEditorInput[]; + readonly resources?: IMultiDiffEditorResource[]; /** * Whether the editor should be serialized and stored for subsequent sessions. @@ -555,6 +555,9 @@ export interface IResourceMultiDiffEditorInput extends IBaseUntypedEditorInput { readonly isTransient?: boolean; } +export interface IMultiDiffEditorResource extends IResourceDiffEditorInput { + readonly goToFileResource?: URI; +} export type IResourceMergeEditorInputSide = (IResourceEditorInput | ITextResourceEditorInput) & { detail?: string }; /** @@ -1079,6 +1082,12 @@ export interface IEditorCommandsContext { preserveFocus?: boolean; } +export function isEditorCommandsContext(context: unknown): context is IEditorCommandsContext { + const candidate = context as IEditorCommandsContext | undefined; + + return typeof candidate?.groupId === 'number'; +} + /** * More information around why an editor was closed in the model. */ @@ -1162,6 +1171,9 @@ export const enum GroupModelChangeKind { GROUP_LABEL, GROUP_LOCKED, + /* Editors Change */ + EDITORS_SELECTION, + /* Editor Changes */ EDITOR_OPEN, EDITOR_CLOSE, @@ -1207,6 +1219,7 @@ interface IEditorPartConfiguration { tabActionLocation?: 'left' | 'right'; tabActionCloseVisibility?: boolean; tabActionUnpinVisibility?: boolean; + alwaysShowEditorActions?: boolean; tabSizing?: 'fit' | 'shrink' | 'fixed'; tabSizingFixedMinWidth?: number; tabSizingFixedMaxWidth?: number; diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 60047f630e3..6f04a87f062 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -25,6 +25,7 @@ export interface IEditorOpenOptions { readonly sticky?: boolean; readonly transient?: boolean; active?: boolean; + readonly inactiveSelection?: EditorInput[]; readonly index?: number; readonly supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; } @@ -174,6 +175,7 @@ export interface IReadonlyEditorGroupModel { readonly isLocked: boolean; readonly activeEditor: EditorInput | null; readonly previewEditor: EditorInput | null; + readonly selectedEditors: EditorInput[]; getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[]; getEditorByIndex(index: number): EditorInput | undefined; @@ -181,6 +183,7 @@ export interface IReadonlyEditorGroupModel { isActive(editor: EditorInput | IUntypedEditorInput): boolean; isPinned(editorOrIndex: EditorInput | number): boolean; isSticky(editorOrIndex: EditorInput | number): boolean; + isSelected(editorOrIndex: EditorInput | number): boolean; isTransient(editorOrIndex: EditorInput | number): boolean; isFirst(editor: EditorInput, editors?: EditorInput[]): boolean; isLast(editor: EditorInput, editors?: EditorInput[]): boolean; @@ -193,6 +196,7 @@ interface IEditorGroupModel extends IReadonlyEditorGroupModel { closeEditor(editor: EditorInput, context?: EditorCloseContext, openNext?: boolean): IEditorCloseResult | undefined; moveEditor(editor: EditorInput, toIndex: number): EditorInput | undefined; setActive(editor: EditorInput | undefined): EditorInput | undefined; + setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): void; } export class EditorGroupModel extends Disposable implements IEditorGroupModel { @@ -201,7 +205,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { //#region events - private readonly _onDidModelChange = this._register(new Emitter()); + private readonly _onDidModelChange = this._register(new Emitter({ leakWarningThreshold: 500 /* increased for users with hundreds of inputs opened */ })); readonly onDidModelChange = this._onDidModelChange.event; //#endregion @@ -216,10 +220,15 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private locked = false; - private preview: EditorInput | null = null; // editor in preview state - private active: EditorInput | null = null; // editor in active state - private sticky = -1; // index of first editor in sticky state - private transient = new Set(); // editors in transient state + private selection: EditorInput[] = []; // editors in selected state, first one is active + + private get active(): EditorInput | null { + return this.selection[0] ?? null; + } + + private preview: EditorInput | null = null; // editor in preview state + private sticky = -1; // index of first editor in sticky state + private readonly transient = new Set(); // editors in transient state private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined; private focusRecentEditorAfterClose: boolean | undefined; @@ -287,8 +296,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return this.active; } - isActive(editor: EditorInput | IUntypedEditorInput): boolean { - return this.matches(this.active, editor); + isActive(candidate: EditorInput | IUntypedEditorInput): boolean { + return this.matches(this.active, candidate); } get previewEditor(): EditorInput | null { @@ -299,7 +308,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); const makePinned = options?.pinned || options?.sticky; const makeTransient = !!options?.transient; - const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); + const makeActive = options?.active || !this.activeEditor || (!makePinned && this.preview === this.activeEditor); const existingEditorAndIndex = this.findEditor(candidate, options); @@ -401,10 +410,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { }; this._onDidModelChange.fire(event); - // Handle active - if (makeActive) { - this.doSetActive(newEditor, targetIndex); - } + // Handle active editor / selected editors + this.setSelection(makeActive ? newEditor : this.activeEditor, options?.inactiveSelection ?? []); return { editor: newEditor, @@ -424,10 +431,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.doPin(existingEditor, existingEditorIndex); } - // Activate it - if (makeActive) { - this.doSetActive(existingEditor, existingEditorIndex); - } + // Handle active editor / selected editors + this.setSelection(makeActive ? existingEditor : this.activeEditor, options?.inactiveSelection ?? []); // Respect index if (options && typeof options.index === 'number') { @@ -545,8 +550,9 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const editor = this.editors[index]; const sticky = this.isSticky(index); - // Active Editor closed - if (openNext && this.matches(this.active, editor)) { + // Active editor closed + const isActiveEditor = this.active === editor; + if (openNext && isActiveEditor) { // More than one editor if (this.mru.length > 1) { @@ -561,17 +567,29 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { } } - this.doSetActive(newActive, this.editors.indexOf(newActive)); + // Select editor as active + const newInactiveSelectedEditors = this.selection.filter(selected => selected !== editor && selected !== newActive); + this.doSetSelection(newActive, this.editors.indexOf(newActive), newInactiveSelectedEditors); } - // One Editor + // Last editor closed: clear selection else { - this.active = null; + this.doSetSelection(null, undefined, []); + } + } + + // Inactive editor closed + else if (!isActiveEditor) { + + // Remove editor from inactive selection + if (this.doIsSelected(editor)) { + const newInactiveSelectedEditors = this.selection.filter(selected => selected !== editor && selected !== this.activeEditor); + this.doSetSelection(this.activeEditor, this.indexOf(this.activeEditor), newInactiveSelectedEditors); } } // Preview Editor closed - if (this.matches(this.preview, editor)) { + if (this.preview === editor) { this.preview = null; } @@ -666,30 +684,99 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const [editor, editorIndex] = res; - this.doSetActive(editor, editorIndex); + this.doSetSelection(editor, editorIndex, []); return editor; } - private doSetActive(editor: EditorInput, editorIndex: number): void { - if (this.matches(this.active, editor)) { - return; // already active + get selectedEditors(): EditorInput[] { + return this.editors.filter(editor => this.doIsSelected(editor)); // return in sequential order + } + + isSelected(editorCandidateOrIndex: EditorInput | number): boolean { + let editor: EditorInput | undefined; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; + } else { + editor = this.findEditor(editorCandidateOrIndex)?.[0]; } - this.active = editor; + return !!editor && this.doIsSelected(editor); + } - // Bring to front in MRU list - const mruIndex = this.indexOf(editor, this.mru); - this.mru.splice(mruIndex, 1); - this.mru.unshift(editor); + private doIsSelected(editor: EditorInput): boolean { + return this.selection.includes(editor); + } - // Event - const event: IGroupEditorChangeEvent = { - kind: GroupModelChangeKind.EDITOR_ACTIVE, - editor, - editorIndex - }; - this._onDidModelChange.fire(event); + setSelection(activeSelectedEditorCandidate: EditorInput, inactiveSelectedEditorCandidates: EditorInput[]): void { + const res = this.findEditor(activeSelectedEditorCandidate); + if (!res) { + return; // not found + } + + const [activeSelectedEditor, activeSelectedEditorIndex] = res; + + const inactiveSelectedEditors = new Set(); + for (const inactiveSelectedEditorCandidate of inactiveSelectedEditorCandidates) { + const res = this.findEditor(inactiveSelectedEditorCandidate); + if (!res) { + return; // not found + } + + const [inactiveSelectedEditor] = res; + if (inactiveSelectedEditor === activeSelectedEditor) { + continue; // already selected + } + + inactiveSelectedEditors.add(inactiveSelectedEditor); + } + + this.doSetSelection(activeSelectedEditor, activeSelectedEditorIndex, Array.from(inactiveSelectedEditors)); + } + + private doSetSelection(activeSelectedEditor: EditorInput | null, activeSelectedEditorIndex: number | undefined, inactiveSelectedEditors: EditorInput[]): void { + const previousActiveEditor = this.activeEditor; + const previousSelection = this.selection; + + let newSelection: EditorInput[]; + if (activeSelectedEditor) { + newSelection = [activeSelectedEditor, ...inactiveSelectedEditors]; + } else { + newSelection = []; + } + + // Update selection + this.selection = newSelection; + + // Update active editor if it has changed + const activeEditorChanged = activeSelectedEditor && typeof activeSelectedEditorIndex === 'number' && previousActiveEditor !== activeSelectedEditor; + if (activeEditorChanged) { + + // Bring to front in MRU list + const mruIndex = this.indexOf(activeSelectedEditor, this.mru); + this.mru.splice(mruIndex, 1); + this.mru.unshift(activeSelectedEditor); + + // Event + const event: IGroupEditorChangeEvent = { + kind: GroupModelChangeKind.EDITOR_ACTIVE, + editor: activeSelectedEditor, + editorIndex: activeSelectedEditorIndex + }; + this._onDidModelChange.fire(event); + } + + // Fire event if the selection has changed + if ( + activeEditorChanged || + previousSelection.length !== newSelection.length || + previousSelection.some(editor => !newSelection.includes(editor)) + ) { + const event: IGroupModelChangeEvent = { + kind: GroupModelChangeKind.EDITORS_SELECTION + }; + this._onDidModelChange.fire(event); + } } setIndex(index: number) { @@ -777,12 +864,12 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { } } - isPinned(editorOrIndex: EditorInput | number): boolean { + isPinned(editorCandidateOrIndex: EditorInput | number): boolean { let editor: EditorInput; - if (typeof editorOrIndex === 'number') { - editor = this.editors[editorOrIndex]; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; } else { - editor = editorOrIndex; + editor = editorCandidateOrIndex; } return !this.matches(this.preview, editor); @@ -919,16 +1006,16 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this._onDidModelChange.fire(event); } - isTransient(editorOrIndex: EditorInput | number): boolean { + isTransient(editorCandidateOrIndex: EditorInput | number): boolean { if (this.transient.size === 0) { return false; // no transient editor } let editor: EditorInput | undefined; - if (typeof editorOrIndex === 'number') { - editor = this.editors[editorOrIndex]; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; } else { - editor = this.findEditor(editorOrIndex)?.[0]; + editor = this.findEditor(editorCandidateOrIndex)?.[0]; } return !!editor && this.transient.has(editor); @@ -1079,7 +1166,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { clone.editors = this.editors.slice(0); clone.mru = this.mru.slice(0); clone.preview = this.preview; - clone.active = this.active; + clone.selection = this.selection.slice(0); clone.sticky = this.sticky; // Ensure to register listeners for each editor @@ -1181,7 +1268,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.mru = coalesce(data.mru.map(i => this.editors[i])); - this.active = this.mru[0]; + this.selection = this.mru.length > 0 ? [this.mru[0]] : []; if (typeof data.preview === 'number') { this.preview = this.editors[data.preview]; diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index 390b19874c8..61d4f6a7c80 100644 --- a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -36,11 +36,13 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE get activeEditor(): EditorInput | null { return this.model.activeEditor && this.filter(this.model.activeEditor) ? this.model.activeEditor : null; } get previewEditor(): EditorInput | null { return this.model.previewEditor && this.filter(this.model.previewEditor) ? this.model.previewEditor : null; } + get selectedEditors(): EditorInput[] { return this.model.selectedEditors.filter(e => this.filter(e)); } isPinned(editorOrIndex: EditorInput | number): boolean { return this.model.isPinned(editorOrIndex); } isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } + isSelected(editorOrIndex: EditorInput | number): boolean { return this.model.isSelected(editorOrIndex); } isFirst(editor: EditorInput): boolean { return this.model.isFirst(editor, this.getEditors(EditorsOrder.SEQUENTIAL)); diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 8ca7c968f9c..1e6aba052fd 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground, listActiveSelectionBackground, listActiveSelectionForeground, editorForeground, toolbarHoverBackground, inputBorder, widgetBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground, listActiveSelectionBackground, listActiveSelectionForeground, editorForeground, toolbarHoverBackground, inputBorder, widgetBorder, scrollbarShadow } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; import { ColorScheme } from 'vs/platform/theme/common/theme'; @@ -28,19 +28,9 @@ export function WORKBENCH_BACKGROUND(theme: IColorTheme): Color { //#region Tab Background -export const TAB_ACTIVE_BACKGROUND = registerColor('tab.activeBackground', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('tabActiveBackground', "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_ACTIVE_BACKGROUND = registerColor('tab.activeBackground', editorBackground, localize('tabActiveBackground', "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_UNFOCUSED_ACTIVE_BACKGROUND = registerColor('tab.unfocusedActiveBackground', { - dark: TAB_ACTIVE_BACKGROUND, - light: TAB_ACTIVE_BACKGROUND, - hcDark: TAB_ACTIVE_BACKGROUND, - hcLight: TAB_ACTIVE_BACKGROUND, -}, localize('tabUnfocusedActiveBackground', "Active tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_UNFOCUSED_ACTIVE_BACKGROUND = registerColor('tab.unfocusedActiveBackground', TAB_ACTIVE_BACKGROUND, localize('tabUnfocusedActiveBackground', "Active tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_INACTIVE_BACKGROUND = registerColor('tab.inactiveBackground', { dark: '#2D2D2D', @@ -49,12 +39,7 @@ export const TAB_INACTIVE_BACKGROUND = registerColor('tab.inactiveBackground', { hcLight: null, }, localize('tabInactiveBackground', "Inactive tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedInactiveBackground', { - dark: TAB_INACTIVE_BACKGROUND, - light: TAB_INACTIVE_BACKGROUND, - hcDark: TAB_INACTIVE_BACKGROUND, - hcLight: TAB_INACTIVE_BACKGROUND -}, localize('tabUnfocusedInactiveBackground', "Inactive tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedInactiveBackground', TAB_INACTIVE_BACKGROUND, localize('tabUnfocusedInactiveBackground', "Inactive tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -92,12 +77,7 @@ export const TAB_UNFOCUSED_INACTIVE_FOREGROUND = registerColor('tab.unfocusedIna //#region Tab Hover Foreground/Background -export const TAB_HOVER_BACKGROUND = registerColor('tab.hoverBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabHoverBackground', "Tab background color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_HOVER_BACKGROUND = registerColor('tab.hoverBackground', null, localize('tabHoverBackground', "Tab background color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BACKGROUND = registerColor('tab.unfocusedHoverBackground', { dark: transparent(TAB_HOVER_BACKGROUND, 0.5), @@ -106,12 +86,7 @@ export const TAB_UNFOCUSED_HOVER_BACKGROUND = registerColor('tab.unfocusedHoverB hcLight: null }, localize('tabUnfocusedHoverBackground', "Tab background color in an unfocused group when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_HOVER_FOREGROUND = registerColor('tab.hoverForeground', { - dark: null, - light: null, - hcDark: null, - hcLight: null, -}, localize('tabHoverForeground', "Tab foreground color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_HOVER_FOREGROUND = registerColor('tab.hoverForeground', null, localize('tabHoverForeground', "Tab foreground color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_FOREGROUND = registerColor('tab.unfocusedHoverForeground', { dark: transparent(TAB_HOVER_FOREGROUND, 0.5), @@ -138,12 +113,7 @@ export const TAB_LAST_PINNED_BORDER = registerColor('tab.lastPinnedBorder', { hcLight: contrastBorder }, localize('lastPinnedTabBorder', "Border to separate pinned tabs from other tabs. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_ACTIVE_BORDER = registerColor('tab.activeBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabActiveBorder', "Border on the bottom of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_ACTIVE_BORDER = registerColor('tab.activeBorder', null, localize('tabActiveBorder', "Border on the bottom of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_BORDER = registerColor('tab.unfocusedActiveBorder', { dark: transparent(TAB_ACTIVE_BORDER, 0.5), @@ -166,12 +136,14 @@ export const TAB_UNFOCUSED_ACTIVE_BORDER_TOP = registerColor('tab.unfocusedActiv hcLight: '#B5200D' }, localize('tabActiveUnfocusedBorderTop', "Border to the top of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabHoverBorder', "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_BORDER_TOP = registerColor('tab.selectedBorderTop', TAB_ACTIVE_BORDER_TOP, localize('tabSelectedBorderTop', "Border to the top of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + +export const TAB_SELECTED_BACKGROUND = registerColor('tab.selectedBackground', TAB_ACTIVE_BACKGROUND, localize('tabSelectedBackground', "Background of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + +export const TAB_SELECTED_FOREGROUND = registerColor('tab.selectedForeground', TAB_ACTIVE_FOREGROUND, localize('tabSelectedForeground', "Foreground of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + + +export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', null, localize('tabHoverBorder', "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BORDER = registerColor('tab.unfocusedHoverBorder', { dark: transparent(TAB_HOVER_BORDER, 0.5), @@ -227,19 +199,9 @@ export const TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER = registerColor('tab.unfocus // < --- Editors --- > -export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); +export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', editorBackground, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); -export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('editorGroupEmptyBackground', "Background color of an empty editor group. Editor groups are the containers of editors.")); +export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', null, localize('editorGroupEmptyBackground', "Background color of an empty editor group. Editor groups are the containers of editors.")); export const EDITOR_GROUP_FOCUSED_EMPTY_BORDER = registerColor('editorGroup.focusedEmptyBorder', { dark: null, @@ -255,19 +217,9 @@ export const EDITOR_GROUP_HEADER_TABS_BACKGROUND = registerColor('editorGroupHea hcLight: null }, localize('tabsContainerBackground', "Background color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); -export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); +export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', null, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); -export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('editorGroupHeaderBackground', "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.")); +export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', editorBackground, localize('editorGroupHeaderBackground', "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_BORDER = registerColor('editorGroupHeader.border', { dark: null, @@ -290,19 +242,9 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hcLight: Color.fromHex('#0F4A85').transparent(0.50) }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); -export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', { - dark: editorWidgetForeground, - light: editorWidgetForeground, - hcDark: editorWidgetForeground, - hcLight: editorWidgetForeground -}, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); +export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', editorWidgetForeground, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); -export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', { - dark: editorWidgetBackground, - light: editorWidgetBackground, - hcDark: editorWidgetBackground, - hcLight: editorWidgetBackground -}, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); +export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', editorWidgetBackground, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropIntoPromptBorder', { dark: null, @@ -311,28 +253,13 @@ export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropInt hcLight: contrastBorder }, localize('editorDropIntoPromptBorder', "Border color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); -export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', { - dark: EDITOR_GROUP_BORDER, - light: EDITOR_GROUP_BORDER, - hcDark: EDITOR_GROUP_BORDER, - hcLight: EDITOR_GROUP_BORDER -}, localize('sideBySideEditor.horizontalBorder', "Color to separate two editors from each other when shown side by side in an editor group from top to bottom.")); +export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', EDITOR_GROUP_BORDER, localize('sideBySideEditor.horizontalBorder', "Color to separate two editors from each other when shown side by side in an editor group from top to bottom.")); -export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEditor.verticalBorder', { - dark: EDITOR_GROUP_BORDER, - light: EDITOR_GROUP_BORDER, - hcDark: EDITOR_GROUP_BORDER, - hcLight: EDITOR_GROUP_BORDER -}, localize('sideBySideEditor.verticalBorder', "Color to separate two editors from each other when shown side by side in an editor group from left to right.")); +export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEditor.verticalBorder', EDITOR_GROUP_BORDER, localize('sideBySideEditor.verticalBorder', "Color to separate two editors from each other when shown side by side in an editor group from left to right.")); // < --- Panels --- > -export const PANEL_BACKGROUND = registerColor('panel.background', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('panelBackground', "Panel background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_BACKGROUND = registerColor('panel.background', editorBackground, localize('panelBackground', "Panel background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_BORDER = registerColor('panel.border', { dark: Color.fromHex('#808080').transparent(0.35), @@ -369,19 +296,9 @@ export const PANEL_INPUT_BORDER = registerColor('panelInput.border', { hcLight: inputBorder }, localize('panelInputBorder', "Input box border for inputs in the panel.")); -export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', { - dark: PANEL_ACTIVE_TITLE_FOREGROUND, - light: PANEL_ACTIVE_TITLE_FOREGROUND, - hcDark: PANEL_ACTIVE_TITLE_FOREGROUND, - hcLight: PANEL_ACTIVE_TITLE_FOREGROUND -}, localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', PANEL_ACTIVE_TITLE_FOREGROUND, localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); -export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND -}, localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), @@ -390,43 +307,24 @@ export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader hcLight: null, }, localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', null, localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', { - dark: contrastBorder, - light: contrastBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', contrastBorder, localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { - dark: PANEL_BORDER, - light: PANEL_BORDER, - hcDark: PANEL_BORDER, - hcLight: PANEL_BORDER -}, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_BORDER = registerColor('panelSection.border', PANEL_BORDER, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); + +export const PANEL_STICKY_SCROLL_BACKGROUND = registerColor('panelStickyScroll.background', PANEL_BACKGROUND, localize('panelStickyScrollBackground', "Background color of sticky scroll in the panel.")); + +export const PANEL_STICKY_SCROLL_BORDER = registerColor('panelStickyScroll.border', null, localize('panelStickyScrollBorder', "Border color of sticky scroll in the panel.")); + +export const PANEL_STICKY_SCROLL_SHADOW = registerColor('panelStickyScroll.shadow', scrollbarShadow, localize('panelStickyScrollShadow', "Shadow color of sticky scroll in the panel.")); // < --- Output Editor --> -const OUTPUT_VIEW_BACKGROUND = registerColor('outputView.background', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('outputViewBackground', "Output view background color.")); +const OUTPUT_VIEW_BACKGROUND = registerColor('outputView.background', null, localize('outputViewBackground', "Output view background color.")); -registerColor('outputViewStickyScroll.background', { - dark: OUTPUT_VIEW_BACKGROUND, - light: OUTPUT_VIEW_BACKGROUND, - hcDark: OUTPUT_VIEW_BACKGROUND, - hcLight: OUTPUT_VIEW_BACKGROUND -}, localize('outputViewStickyScrollBackground', "Output view sticky scroll background color.")); +registerColor('outputViewStickyScroll.background', OUTPUT_VIEW_BACKGROUND, localize('outputViewStickyScrollBackground', "Output view sticky scroll background color.")); // < --- Banner --- > @@ -438,19 +336,9 @@ export const BANNER_BACKGROUND = registerColor('banner.background', { hcLight: listActiveSelectionBackground }, localize('banner.background', "Banner background color. The banner is shown under the title bar of the window.")); -export const BANNER_FOREGROUND = registerColor('banner.foreground', { - dark: listActiveSelectionForeground, - light: listActiveSelectionForeground, - hcDark: listActiveSelectionForeground, - hcLight: listActiveSelectionForeground -}, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); +export const BANNER_FOREGROUND = registerColor('banner.foreground', listActiveSelectionForeground, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); -export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', { - dark: editorInfoForeground, - light: editorInfoForeground, - hcDark: editorInfoForeground, - hcLight: editorInfoForeground -}, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); +export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', editorInfoForeground, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); // < --- Status --- > @@ -461,12 +349,7 @@ export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { hcLight: editorForeground }, localize('statusBarForeground', "Status bar foreground color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarNoFolderForeground', "Status bar foreground color when no folder is opened. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', STATUS_BAR_FOREGROUND, localize('statusBarNoFolderForeground', "Status bar foreground color when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_BACKGROUND = registerColor('statusBar.background', { dark: '#007ACC', @@ -496,12 +379,7 @@ export const STATUS_BAR_FOCUS_BORDER = registerColor('statusBar.focusBorder', { hcLight: STATUS_BAR_FOREGROUND }, localize('statusBarFocusBorder', "Status bar border color when focused on keyboard navigation. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_NO_FOLDER_BORDER = registerColor('statusBar.noFolderBorder', { - dark: STATUS_BAR_BORDER, - light: STATUS_BAR_BORDER, - hcDark: STATUS_BAR_BORDER, - hcLight: STATUS_BAR_BORDER -}, localize('statusBarNoFolderBorder', "Status bar border color separating to the sidebar and editor when no folder is opened. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_NO_FOLDER_BORDER = registerColor('statusBar.noFolderBorder', STATUS_BAR_BORDER, localize('statusBarNoFolderBorder', "Status bar border color separating to the sidebar and editor when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_ACTIVE_BACKGROUND = registerColor('statusBarItem.activeBackground', { dark: Color.white.transparent(0.18), @@ -524,12 +402,7 @@ export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hov hcLight: Color.black.transparent(0.12) }, localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', STATUS_BAR_FOREGROUND, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBarItem.compactHoverBackground', { dark: Color.white.transparent(0.20), @@ -538,26 +411,11 @@ export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBar hcLight: Color.black.transparent(0.20) }, localize('statusBarItemCompactHoverBackground', "Status bar item background color when hovering an item that contains two hovers. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', STATUS_BAR_FOREGROUND, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', { - dark: Color.black.transparent(0.5), - light: Color.black.transparent(0.5), - hcDark: Color.black.transparent(0.5), - hcLight: Color.black.transparent(0.5), -}, localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', Color.black.transparent(0.5), localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_PROMINENT_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.prominentHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarProminentItemHoverForeground', "Status bar prominent items foreground color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.prominentHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarProminentItemHoverForeground', "Status bar prominent items foreground color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.prominentHoverBackground', { dark: Color.black.transparent(0.3), @@ -573,26 +431,11 @@ export const STATUS_BAR_ERROR_ITEM_BACKGROUND = registerColor('statusBarItem.err hcLight: '#B5200D' }, localize('statusBarErrorItemBackground', "Status bar error items background color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.errorForeground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.errorForeground', Color.white, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.errorHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarErrorItemHoverForeground', "Status bar error items foreground color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.errorHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarErrorItemHoverForeground', "Status bar error items foreground color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.errorHoverBackground', { - dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - light: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND -}, localize('statusBarErrorItemHoverBackground', "Status bar error items background color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.errorHoverBackground', STATUS_BAR_ITEM_HOVER_BACKGROUND, localize('statusBarErrorItemHoverBackground', "Status bar error items background color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.warningBackground', { dark: darken(editorWarningForeground, .4), @@ -601,26 +444,11 @@ export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.w hcLight: '#895503' }, localize('statusBarWarningItemBackground', "Status bar warning items background color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.warningForeground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.warningForeground', Color.white, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.warningHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarWarningItemHoverForeground', "Status bar warning items foreground color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.warningHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarWarningItemHoverForeground', "Status bar warning items foreground color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.warningHoverBackground', { - dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - light: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND -}, localize('statusBarWarningItemHoverBackground', "Status bar warning items background color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.warningHoverBackground', STATUS_BAR_ITEM_HOVER_BACKGROUND, localize('statusBarWarningItemHoverBackground', "Status bar warning items background color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); // < --- Activity Bar --- > @@ -656,7 +484,7 @@ export const ACTIVITY_BAR_BORDER = registerColor('activityBar.border', { export const ACTIVITY_BAR_ACTIVE_BORDER = registerColor('activityBar.activeBorder', { dark: ACTIVITY_BAR_FOREGROUND, light: ACTIVITY_BAR_FOREGROUND, - hcDark: null, + hcDark: contrastBorder, hcLight: contrastBorder }, localize('activityBarActiveBorder', "Activity bar border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); @@ -667,12 +495,7 @@ export const ACTIVITY_BAR_ACTIVE_FOCUS_BORDER = registerColor('activityBar.activ hcLight: '#B5200D' }, localize('activityBarActiveFocusBorder', "Activity bar focus border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', null, localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_DRAG_AND_DROP_BORDER = registerColor('activityBar.dropBorder', { dark: ACTIVITY_BAR_FOREGROUND, @@ -688,12 +511,7 @@ export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.bac hcLight: '#0F4A85' }, localize('activityBarBadgeBackground', "Activity notification badge background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', Color.white, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_FOREGROUND = registerColor('activityBarTop.foreground', { dark: '#E7E7E7', @@ -709,12 +527,7 @@ export const ACTIVITY_BAR_TOP_ACTIVE_BORDER = registerColor('activityBarTop.acti hcLight: '#B5200D' }, localize('activityBarTopActiveFocusBorder', "Focus border color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_ACTIVE_BACKGROUND = registerColor('activityBarTop.activeBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('activityBarTopActiveBackground', "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_TOP_ACTIVE_BACKGROUND = registerColor('activityBarTop.activeBackground', null, localize('activityBarTopActiveBackground', "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTop.inactiveForeground', { dark: transparent(ACTIVITY_BAR_TOP_FOREGROUND, 0.6), @@ -723,19 +536,9 @@ export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTo hcLight: editorForeground }, localize('activityBarTopInActiveForeground', "Inactive foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', { - dark: ACTIVITY_BAR_TOP_FOREGROUND, - light: ACTIVITY_BAR_TOP_FOREGROUND, - hcDark: ACTIVITY_BAR_TOP_FOREGROUND, - hcLight: ACTIVITY_BAR_TOP_FOREGROUND -}, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', ACTIVITY_BAR_TOP_FOREGROUND, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', { - dark: null, - light: null, - hcDark: null, - hcLight: null, -}, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); +export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', null, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); // < --- Profiles --- > @@ -756,26 +559,11 @@ export const PROFILE_BADGE_FOREGROUND = registerColor('profileBadge.foreground', // < --- Remote --- > -export const STATUS_BAR_REMOTE_ITEM_BACKGROUND = registerColor('statusBarItem.remoteBackground', { - dark: ACTIVITY_BAR_BADGE_BACKGROUND, - light: ACTIVITY_BAR_BADGE_BACKGROUND, - hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, - hcLight: ACTIVITY_BAR_BADGE_BACKGROUND -}, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); +export const STATUS_BAR_REMOTE_ITEM_BACKGROUND = registerColor('statusBarItem.remoteBackground', ACTIVITY_BAR_BADGE_BACKGROUND, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); -export const STATUS_BAR_REMOTE_ITEM_FOREGROUND = registerColor('statusBarItem.remoteForeground', { - dark: ACTIVITY_BAR_BADGE_FOREGROUND, - light: ACTIVITY_BAR_BADGE_FOREGROUND, - hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, - hcLight: ACTIVITY_BAR_BADGE_FOREGROUND -}, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); +export const STATUS_BAR_REMOTE_ITEM_FOREGROUND = registerColor('statusBarItem.remoteForeground', ACTIVITY_BAR_BADGE_FOREGROUND, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); -export const STATUS_BAR_REMOTE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.remoteHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarRemoteItemHoverForeground', "Foreground color for the remote indicator on the status bar when hovering.")); +export const STATUS_BAR_REMOTE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.remoteHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarRemoteItemHoverForeground', "Foreground color for the remote indicator on the status bar when hovering.")); export const STATUS_BAR_REMOTE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.remoteHoverBackground', { dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, @@ -784,26 +572,11 @@ export const STATUS_BAR_REMOTE_ITEM_HOVER_BACKGROUND = registerColor('statusBarI hcLight: null }, localize('statusBarRemoteItemHoverBackground', "Background color for the remote indicator on the status bar when hovering.")); -export const STATUS_BAR_OFFLINE_ITEM_BACKGROUND = registerColor('statusBarItem.offlineBackground', { - dark: '#6c1717', - light: '#6c1717', - hcDark: '#6c1717', - hcLight: '#6c1717' -}, localize('statusBarItemOfflineBackground', "Status bar item background color when the workbench is offline.")); +export const STATUS_BAR_OFFLINE_ITEM_BACKGROUND = registerColor('statusBarItem.offlineBackground', '#6c1717', localize('statusBarItemOfflineBackground', "Status bar item background color when the workbench is offline.")); -export const STATUS_BAR_OFFLINE_ITEM_FOREGROUND = registerColor('statusBarItem.offlineForeground', { - dark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - light: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - hcDark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - hcLight: STATUS_BAR_REMOTE_ITEM_FOREGROUND -}, localize('statusBarItemOfflineForeground', "Status bar item foreground color when the workbench is offline.")); +export const STATUS_BAR_OFFLINE_ITEM_FOREGROUND = registerColor('statusBarItem.offlineForeground', STATUS_BAR_REMOTE_ITEM_FOREGROUND, localize('statusBarItemOfflineForeground', "Status bar item foreground color when the workbench is offline.")); -export const STATUS_BAR_OFFLINE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.offlineHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarOfflineItemHoverForeground', "Status bar item foreground hover color when the workbench is offline.")); +export const STATUS_BAR_OFFLINE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.offlineHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarOfflineItemHoverForeground', "Status bar item foreground hover color when the workbench is offline.")); export const STATUS_BAR_OFFLINE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.offlineHoverBackground', { dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, @@ -812,19 +585,9 @@ export const STATUS_BAR_OFFLINE_ITEM_HOVER_BACKGROUND = registerColor('statusBar hcLight: null }, localize('statusBarOfflineItemHoverBackground', "Status bar item background hover color when the workbench is offline.")); -export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', { - dark: ACTIVITY_BAR_BADGE_BACKGROUND, - light: ACTIVITY_BAR_BADGE_BACKGROUND, - hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, - hcLight: ACTIVITY_BAR_BADGE_BACKGROUND -}, localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view.")); +export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', ACTIVITY_BAR_BADGE_BACKGROUND, localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view.")); -export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', { - dark: ACTIVITY_BAR_BADGE_FOREGROUND, - light: ACTIVITY_BAR_BADGE_FOREGROUND, - hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, - hcLight: ACTIVITY_BAR_BADGE_FOREGROUND -}, localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view.")); +export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', ACTIVITY_BAR_BADGE_FOREGROUND, localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view.")); // < --- Side Bar --- > @@ -836,12 +599,7 @@ export const SIDE_BAR_BACKGROUND = registerColor('sideBar.background', { hcLight: '#FFFFFF' }, localize('sideBarBackground', "Side bar background color. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_FOREGROUND = registerColor('sideBar.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('sideBarForeground', "Side bar foreground color. The side bar is the container for views like explorer and search.")); +export const SIDE_BAR_FOREGROUND = registerColor('sideBar.foreground', null, localize('sideBarForeground', "Side bar foreground color. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_BORDER = registerColor('sideBar.border', { dark: null, @@ -850,19 +608,11 @@ export const SIDE_BAR_BORDER = registerColor('sideBar.border', { hcLight: contrastBorder }, localize('sideBarBorder', "Side bar border color on the side separating to the editor. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground', { - dark: SIDE_BAR_FOREGROUND, - light: SIDE_BAR_FOREGROUND, - hcDark: SIDE_BAR_FOREGROUND, - hcLight: SIDE_BAR_FOREGROUND -}, localize('sideBarTitleForeground', "Side bar title foreground color. The side bar is the container for views like explorer and search.")); +export const SIDE_BAR_TITLE_BACKGROUND = registerColor('sideBarTitle.background', SIDE_BAR_BACKGROUND, localize('sideBarTitleBackground', "Side bar title background color. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND -}, localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground', SIDE_BAR_FOREGROUND, localize('sideBarTitleForeground', "Side bar title foreground color. The side bar is the container for views like explorer and search.")); + +export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), @@ -871,26 +621,17 @@ export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionH hcLight: null }, localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); -export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', { - dark: SIDE_BAR_FOREGROUND, - light: SIDE_BAR_FOREGROUND, - hcDark: SIDE_BAR_FOREGROUND, - hcLight: SIDE_BAR_FOREGROUND -}, localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', SIDE_BAR_FOREGROUND, localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); -export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', { - dark: contrastBorder, - light: contrastBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', contrastBorder, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); -export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', { - dark: SIDE_BAR_SECTION_HEADER_BORDER, - light: SIDE_BAR_SECTION_HEADER_BORDER, - hcDark: SIDE_BAR_SECTION_HEADER_BORDER, - hcLight: SIDE_BAR_SECTION_HEADER_BORDER -}, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); +export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', SIDE_BAR_SECTION_HEADER_BORDER, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); + +export const SIDE_BAR_STICKY_SCROLL_BACKGROUND = registerColor('sideBarStickyScroll.background', SIDE_BAR_BACKGROUND, localize('sideBarStickyScrollBackground', "Background color of sticky scroll in the side bar.")); + +export const SIDE_BAR_STICKY_SCROLL_BORDER = registerColor('sideBarStickyScroll.border', null, localize('sideBarStickyScrollBorder', "Border color of sticky scroll in the side bar.")); + +export const SIDE_BAR_STICKY_SCROLL_SHADOW = registerColor('sideBarStickyScroll.shadow', scrollbarShadow, localize('sideBarStickyScrollShadow', "Shadow color of sticky scroll in the side bar.")); // < --- Title Bar --- > @@ -931,12 +672,7 @@ export const TITLE_BAR_BORDER = registerColor('titleBar.border', { // < --- Menubar --- > -export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', { - dark: TITLE_BAR_ACTIVE_FOREGROUND, - light: TITLE_BAR_ACTIVE_FOREGROUND, - hcDark: TITLE_BAR_ACTIVE_FOREGROUND, - hcLight: TITLE_BAR_ACTIVE_FOREGROUND, -}, localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); +export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', TITLE_BAR_ACTIVE_FOREGROUND, localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); export const MENUBAR_SELECTION_BACKGROUND = registerColor('menubar.selectionBackground', { dark: toolbarHoverBackground, @@ -957,19 +693,19 @@ export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', // foreground (inactive and active) export const COMMAND_CENTER_FOREGROUND = registerColor( 'commandCenter.foreground', - { dark: TITLE_BAR_ACTIVE_FOREGROUND, hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, + TITLE_BAR_ACTIVE_FOREGROUND, localize('commandCenter-foreground', "Foreground color of the command center"), false ); export const COMMAND_CENTER_ACTIVEFOREGROUND = registerColor( 'commandCenter.activeForeground', - { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, + MENUBAR_SELECTION_FOREGROUND, localize('commandCenter-activeForeground', "Active foreground color of the command center"), false ); export const COMMAND_CENTER_INACTIVEFOREGROUND = registerColor( 'commandCenter.inactiveForeground', - { dark: TITLE_BAR_INACTIVE_FOREGROUND, hcDark: TITLE_BAR_INACTIVE_FOREGROUND, light: TITLE_BAR_INACTIVE_FOREGROUND, hcLight: TITLE_BAR_INACTIVE_FOREGROUND }, + TITLE_BAR_INACTIVE_FOREGROUND, localize('commandCenter-inactiveForeground', "Foreground color of the command center when the window is inactive"), false ); @@ -999,7 +735,7 @@ export const COMMAND_CENTER_ACTIVEBORDER = registerColor( ); // border: defaults to active background export const COMMAND_CENTER_INACTIVEBORDER = registerColor( - 'commandCenter.inactiveBorder', { dark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcDark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), light: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcLight: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25) }, + 'commandCenter.inactiveBorder', transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), localize('commandCenter-inactiveBorder', "Border color of the command center when the window is inactive"), false ); @@ -1021,33 +757,13 @@ export const NOTIFICATIONS_TOAST_BORDER = registerColor('notificationToast.borde hcLight: contrastBorder }, localize('notificationToastBorder', "Notification toast border color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_FOREGROUND = registerColor('notifications.foreground', { - dark: editorWidgetForeground, - light: editorWidgetForeground, - hcDark: editorWidgetForeground, - hcLight: editorWidgetForeground -}, localize('notificationsForeground', "Notifications foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_FOREGROUND = registerColor('notifications.foreground', editorWidgetForeground, localize('notificationsForeground', "Notifications foreground color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_BACKGROUND = registerColor('notifications.background', { - dark: editorWidgetBackground, - light: editorWidgetBackground, - hcDark: editorWidgetBackground, - hcLight: editorWidgetBackground -}, localize('notificationsBackground', "Notifications background color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_BACKGROUND = registerColor('notifications.background', editorWidgetBackground, localize('notificationsBackground', "Notifications background color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_LINKS = registerColor('notificationLink.foreground', { - dark: textLinkForeground, - light: textLinkForeground, - hcDark: textLinkForeground, - hcLight: textLinkForeground -}, localize('notificationsLink', "Notification links foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_LINKS = registerColor('notificationLink.foreground', textLinkForeground, localize('notificationsLink', "Notification links foreground color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_CENTER_HEADER_FOREGROUND = registerColor('notificationCenterHeader.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('notificationCenterHeaderForeground', "Notifications center header foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_CENTER_HEADER_FOREGROUND = registerColor('notificationCenterHeader.foreground', null, localize('notificationCenterHeaderForeground', "Notifications center header foreground color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_CENTER_HEADER_BACKGROUND = registerColor('notificationCenterHeader.background', { dark: lighten(NOTIFICATIONS_BACKGROUND, 0.3), @@ -1056,33 +772,13 @@ export const NOTIFICATIONS_CENTER_HEADER_BACKGROUND = registerColor('notificatio hcLight: NOTIFICATIONS_BACKGROUND }, localize('notificationCenterHeaderBackground', "Notifications center header background color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_BORDER = registerColor('notifications.border', { - dark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - light: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - hcDark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - hcLight: NOTIFICATIONS_CENTER_HEADER_BACKGROUND -}, localize('notificationsBorder', "Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_BORDER = registerColor('notifications.border', NOTIFICATIONS_CENTER_HEADER_BACKGROUND, localize('notificationsBorder', "Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_ERROR_ICON_FOREGROUND = registerColor('notificationsErrorIcon.foreground', { - dark: editorErrorForeground, - light: editorErrorForeground, - hcDark: editorErrorForeground, - hcLight: editorErrorForeground -}, localize('notificationsErrorIconForeground', "The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_ERROR_ICON_FOREGROUND = registerColor('notificationsErrorIcon.foreground', editorErrorForeground, localize('notificationsErrorIconForeground', "The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_WARNING_ICON_FOREGROUND = registerColor('notificationsWarningIcon.foreground', { - dark: editorWarningForeground, - light: editorWarningForeground, - hcDark: editorWarningForeground, - hcLight: editorWarningForeground -}, localize('notificationsWarningIconForeground', "The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_WARNING_ICON_FOREGROUND = registerColor('notificationsWarningIcon.foreground', editorWarningForeground, localize('notificationsWarningIconForeground', "The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_INFO_ICON_FOREGROUND = registerColor('notificationsInfoIcon.foreground', { - dark: editorInfoForeground, - light: editorInfoForeground, - hcDark: editorInfoForeground, - hcLight: editorInfoForeground -}, localize('notificationsInfoIconForeground', "The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_INFO_ICON_FOREGROUND = registerColor('notificationsInfoIcon.foreground', editorInfoForeground, localize('notificationsInfoIconForeground', "The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.")); export const WINDOW_ACTIVE_BORDER = registerColor('window.activeBorder', { dark: null, diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 8a2dca5944c..7d0bb72c307 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -15,12 +15,11 @@ import { getOrSet, SetMap } from 'vs/base/common/map'; import { Registry } from 'vs/platform/registry/common/platform'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { flatten } from 'vs/base/common/arrays'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import Severity from 'vs/base/common/severity'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -204,7 +203,7 @@ class ViewContainersRegistryImpl extends Disposable implements IViewContainersRe private readonly defaultViewContainers: ViewContainer[] = []; get all(): ViewContainer[] { - return flatten([...this.viewContainers.values()]); + return [...this.viewContainers.values()].flat(); } registerViewContainer(viewContainerDescriptor: IViewContainerDescriptor, viewContainerLocation: ViewContainerLocation, options?: { isDefault?: boolean; doNotRegisterOpenCommand?: boolean }): ViewContainer { @@ -300,6 +299,8 @@ export interface IViewDescriptor { readonly virtualWorkspace?: string; readonly openCommandActionDescriptor?: OpenCommandActionDescriptor; + + readonly accessibilityHelpContent?: MarkdownString; } export interface ICustomViewDescriptor extends IViewDescriptor { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index 945637c5dfb..9a00c6035bf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -8,28 +8,32 @@ import { DynamicSpeechAccessibilityConfiguration, registerAccessibilityConfigura import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IAccessibleViewService, AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution'; -import { HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal'; -import { CommentsAccessibilityHelpContribution } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; -import { DiffEditorActiveAnnouncementContribution } from 'vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement'; +import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/saveAccessibilitySignal'; +import { DiffEditorActiveAnnouncementContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement'; +import { SpeechAccessibilitySignalContribution } from 'vs/workbench/contrib/speech/browser/speechAccessibilitySignal'; +import { AccessibleViewInformationService, IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccesibleViewHelpContribution, AccesibleViewContributions } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; +import { ExtensionAccessibilityHelpDialogContribution } from 'vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution'; registerAccessibilityConfiguration(); registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed); +registerSingleton(IAccessibleViewInformationService, AccessibleViewInformationService, InstantiationType.Delayed); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(EditorAccessibilityHelpContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(CommentsAccessibilityHelpContribution, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(UnfocusedViewDimmingContribution, LifecyclePhase.Restored); -workbenchRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(AccesibleViewHelpContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(AccesibleViewContributions, LifecyclePhase.Eventually); registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ExtensionAccessibilityHelpDialogContribution.ID, ExtensionAccessibilityHelpDialogContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(SaveAccessibilitySignalContribution.ID, SaveAccessibilitySignalContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(SpeechAccessibilitySignalContribution.ID, SpeechAccessibilitySignalContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(DiffEditorActiveAnnouncementContribution.ID, DiffEditorActiveAnnouncementContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(DynamicSpeechAccessibilityConfiguration.ID, DynamicSpeechAccessibilityConfiguration, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 51e86960f7c..4948535d753 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -7,13 +7,14 @@ import { localize } from 'vs/nls'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { workbenchConfigurationNodeBase, Extensions as WorkbenchExtensions, IConfigurationMigrationRegistry, ConfigurationKeyValuePairs } from 'vs/workbench/common/configuration'; -import { AccessibilityAlertSettingId, AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { ISpeechService, SPEECH_LANGUAGES, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; +import { workbenchConfigurationNodeBase, Extensions as WorkbenchExtensions, IConfigurationMigrationRegistry, ConfigurationKeyValuePairs, ConfigurationMigration } from 'vs/workbench/common/configuration'; +import { AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { AccessibilityVoiceSettingId, ISpeechService, SPEECH_LANGUAGES } from 'vs/workbench/contrib/speech/common/speechService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Event } from 'vs/base/common/event'; -import { soundFeatureBase } from 'vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution'; +import { isDefined } from 'vs/base/common/types'; +import { IProductService } from 'vs/platform/product/common/productService'; export const accessibilityHelpIsShown = new RawContextKey('accessibilityHelpIsShown', false, true); export const accessibleViewIsShown = new RawContextKey('accessibleViewIsShown', false, true); @@ -24,6 +25,8 @@ export const accessibleViewOnLastLine = new RawContextKey('accessibleVi export const accessibleViewCurrentProviderId = new RawContextKey('accessibleViewCurrentProviderId', undefined, undefined); export const accessibleViewInCodeBlock = new RawContextKey('accessibleViewInCodeBlock', undefined, undefined); export const accessibleViewContainsCodeBlocks = new RawContextKey('accessibleViewContainsCodeBlocks', undefined, undefined); +export const accessibleViewHasUnassignedKeybindings = new RawContextKey('accessibleViewHasUnassignedKeybindings', undefined, undefined); +export const accessibleViewHasAssignedKeybindings = new RawContextKey('accessibleViewHasAssignedKeybindings', undefined, undefined); /** * Miscellaneous settings tagged with accessibility and implemented in the accessibility contrib but @@ -55,25 +58,10 @@ export const enum AccessibilityVerbositySettingId { Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', + ReplInputHint = 'accessibility.verbosity.replInputHint', Comments = 'accessibility.verbosity.comments', - DiffEditorActive = 'accessibility.verbosity.diffEditorActive' -} - -export const enum AccessibleViewProviderId { - Terminal = 'terminal', - TerminalChat = 'terminal-chat', - TerminalHelp = 'terminal-help', - DiffEditor = 'diffEditor', - Chat = 'panelChat', - InlineChat = 'inlineChat', - InlineCompletions = 'inlineCompletions', - KeybindingsEditor = 'keybindingsEditor', - Notebook = 'notebook', - Editor = 'editor', - Hover = 'hover', - Notification = 'notification', - EmptyEditorHint = 'emptyEditorHint', - Comments = 'comments' + DiffEditorActive = 'accessibility.verbosity.diffEditorActive', + Debug = 'accessibility.verbosity.debug', } const baseVerbosityProperty: IConfigurationPropertySchema = { @@ -81,13 +69,6 @@ const baseVerbosityProperty: IConfigurationPropertySchema = { default: true, tags: ['accessibility'] }; -const markdownDeprecationMessage = localize('accessibility.announcement.deprecationMessage', "This setting is deprecated. Use the `signals` settings instead."); -const baseAlertProperty: IConfigurationPropertySchema = { - type: 'boolean', - default: true, - tags: ['accessibility'], - markdownDeprecationMessage -}; export const accessibilityConfigurationNodeBase = Object.freeze({ id: 'accessibility', @@ -95,6 +76,17 @@ export const accessibilityConfigurationNodeBase = Object.freeze languages[key].name), 'enumItemLabels': languagesSorted.map(key => languages[key].name) + }, + [AccessibilityVoiceSettingId.AutoSynthesize]: { + 'type': 'string', + 'enum': ['on', 'off', 'auto'], + 'enumDescriptions': [ + localize('accessibility.voice.autoSynthesize.on', "Enable the feature. When a screen reader is enabled, note that this will disable aria updates."), + localize('accessibility.voice.autoSynthesize.off', "Disable the feature."), + localize('accessibility.voice.autoSynthesize.auto', "When a screen reader is detected, disable the feature. Otherwise, enable the feature.") + ], + 'markdownDescription': localize('autoSynthesize', "Whether a textual response should automatically be read out aloud when speech was used as input. For example in a chat session, a response is automatically synthesized when voice was used as chat request."), + 'default': this.productService.quality !== 'stable' ? 'auto' : 'off', // TODO@bpasero decide on a default + 'tags': ['accessibility'] } } }); @@ -762,7 +793,7 @@ Registry.as(WorkbenchExtensions.ConfigurationMi key: 'audioCues.volume', migrateFn: (value, accessor) => { return [ - ['accessibility.signals.sounds.volume', { value }], + ['accessibility.signalOptions.volume', { value }], ['audioCues.volume', { value: undefined }] ]; } @@ -771,16 +802,98 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'audioCues.debouncePositionChanges', - migrateFn: (value, accessor) => { + migrateFn: (value) => { return [ - ['accessibility.signals.debouncePositionChanges', { value }], + ['accessibility.signalOptions.debouncePositionChanges', { value }], ['audioCues.debouncePositionChanges', { value: undefined }] ]; } }]); Registry.as(WorkbenchExtensions.ConfigurationMigration) - .registerConfigurationMigrations(AccessibilitySignal.allAccessibilitySignals.map(item => ({ + .registerConfigurationMigrations([{ + key: 'accessibility.signalOptions', + migrateFn: (value, accessor) => { + const delayGeneral = getDelaysFromConfig(accessor, 'general'); + const delayError = getDelaysFromConfig(accessor, 'errorAtPosition'); + const delayWarning = getDelaysFromConfig(accessor, 'warningAtPosition'); + const volume = getVolumeFromConfig(accessor); + const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); + return [ + ['accessibility.signalOptions.volume', { value: volume }], + ['accessibility.signalOptions.debouncePositionChanges', { value: debouncePositionChanges }], + ['accessibility.signalOptions.experimental.delays.general', { value: delayGeneral }], + ['accessibility.signalOptions.experimental.delays.errorAtPosition', { value: delayError }], + ['accessibility.signalOptions.experimental.delays.warningAtPosition', { value: delayWarning }], + ['accessibility.signalOptions', { value: undefined }], + ]; + } + }]); + + +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: 'accessibility.signals.sounds.volume', + migrateFn: (value) => { + return [ + ['accessibility.signalOptions.volume', { value }], + ['accessibility.signals.sounds.volume', { value: undefined }] + ]; + } + }]); + +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: 'accessibility.signals.debouncePositionChanges', + migrateFn: (value) => { + return [ + ['accessibility.signalOptions.debouncePositionChanges', { value }], + ['accessibility.signals.debouncePositionChanges', { value: undefined }] + ]; + } + }]); + +function getDelaysFromConfig(accessor: (key: string) => any, type: 'general' | 'errorAtPosition' | 'warningAtPosition'): { announcement: number; sound: number } | undefined { + return accessor(`accessibility.signalOptions.experimental.delays.${type}`) || accessor('accessibility.signalOptions')?.['experimental.delays']?.[`${type}`] || accessor('accessibility.signalOptions')?.['delays']?.[`${type}`]; +} + +function getVolumeFromConfig(accessor: (key: string) => any): string | undefined { + return accessor('accessibility.signalOptions.volume') || accessor('accessibility.signalOptions')?.volume || accessor('accessibility.signals.sounds.volume') || accessor('audioCues.volume'); +} + +function getDebouncePositionChangesFromConfig(accessor: (key: string) => any): number | undefined { + return accessor('accessibility.signalOptions.debouncePositionChanges') || accessor('accessibility.signalOptions')?.debouncePositionChanges || accessor('accessibility.signals.debouncePositionChanges') || accessor('audioCues.debouncePositionChanges'); +} + +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: AccessibilityVoiceSettingId.AutoSynthesize, + migrateFn: (value: boolean) => { + let newValue: string | undefined; + if (value === true) { + newValue = 'on'; + } else if (value === false) { + newValue = 'off'; + } + return [ + [AccessibilityVoiceSettingId.AutoSynthesize, { value: newValue }], + ]; + } + }]); + +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: 'accessibility.signals.chatResponsePending', + migrateFn: (value, accessor) => { + return [ + ['accessibility.signals.progress', { value }], + ['accessibility.signals.chatResponsePending', { value: undefined }], + ]; + } + }]); + +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations(AccessibilitySignal.allAccessibilitySignals.map(item => item.legacySoundSettingsKey ? ({ key: item.legacySoundSettingsKey, migrateFn: (sound, accessor) => { const configurationKeyValuePairs: ConfigurationKeyValuePairs = []; @@ -796,14 +909,14 @@ Registry.as(WorkbenchExtensions.ConfigurationMi configurationKeyValuePairs.push([`${item.settingsKey}`, { value: announcement !== undefined ? { announcement, sound } : { sound } }]); return configurationKeyValuePairs; } - }))); + }) : undefined).filter(isDefined)); Registry.as(WorkbenchExtensions.ConfigurationMigration) - .registerConfigurationMigrations(AccessibilitySignal.allAccessibilitySignals.filter(i => !!i.legacyAnnouncementSettingsKey).map(item => ({ + .registerConfigurationMigrations(AccessibilitySignal.allAccessibilitySignals.filter(i => !!i.legacyAnnouncementSettingsKey && !!i.legacySoundSettingsKey).map(item => ({ key: item.legacyAnnouncementSettingsKey!, migrateFn: (announcement, accessor) => { const configurationKeyValuePairs: ConfigurationKeyValuePairs = []; - const sound = accessor(item.settingsKey)?.sound || accessor(item.legacySoundSettingsKey); + const sound = accessor(item.settingsKey)?.sound || accessor(item.legacySoundSettingsKey!); if (announcement !== undefined && typeof announcement !== 'string') { announcement = announcement ? 'auto' : 'off'; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts deleted file mode 100644 index 30756d73984..00000000000 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts +++ /dev/null @@ -1,275 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { localize } from 'vs/nls'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import * as strings from 'vs/base/common/strings'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hover'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { getNotificationFromContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; -import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; -import { NotificationFocusedContext } from 'vs/workbench/common/contextkeys'; -import { IAccessibleViewService, IAccessibleViewOptions, AccessibleViewType } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { alert } from 'vs/base/browser/ui/aria/aria'; -import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { IAction } from 'vs/base/common/actions'; -import { INotificationViewItem } from 'vs/workbench/common/notifications'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { Codicon } from 'vs/base/common/codicons'; -import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; -import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; - -export function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return strings.format(msg, kb.getAriaLabel()); - } - return strings.format(noKbMsg, commandId); -} - -export class HoverAccessibleViewContribution extends Disposable { - static ID: 'hoverAccessibleViewContribution'; - private _options: IAccessibleViewOptions = { language: 'typescript', type: AccessibleViewType.View }; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(95, 'hover', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined; - if (!editor || !editorHoverContent) { - return false; - } - this._options.language = editor?.getModel()?.getLanguageId() ?? undefined; - accessibleViewService.show({ - id: AccessibleViewProviderId.Hover, - verbositySettingKey: AccessibilityVerbositySettingId.Hover, - provideContent() { return editorHoverContent; }, - onClose() { - HoverController.get(editor)?.focus(); - }, - options: this._options - }); - return true; - }, EditorContextKeys.hoverFocused)); - this._register(AccessibleViewAction.addImplementation(90, 'extension-hover', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const contextViewService = accessor.get(IContextViewService); - const contextViewElement = contextViewService.getContextViewElement(); - const extensionHoverContent = contextViewElement?.textContent ?? undefined; - const hoverService = accessor.get(IHoverService); - - if (contextViewElement.classList.contains('accessible-view-container') || !extensionHoverContent) { - // The accessible view, itself, uses the context view service to display the text. We don't want to read that. - return false; - } - accessibleViewService.show({ - id: AccessibleViewProviderId.Hover, - verbositySettingKey: AccessibilityVerbositySettingId.Hover, - provideContent() { return extensionHoverContent; }, - onClose() { - hoverService.showAndFocusLastHover(); - }, - options: this._options - }); - return true; - })); - this._register(AccessibilityHelpAction.addImplementation(115, 'accessible-view', accessor => { - accessor.get(IAccessibleViewService).showAccessibleViewHelp(); - return true; - }, accessibleViewIsShown)); - } -} - -export class NotificationAccessibleViewContribution extends Disposable { - static ID: 'notificationAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(90, 'notifications', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const listService = accessor.get(IListService); - const commandService = accessor.get(ICommandService); - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - - function renderAccessibleView(): boolean { - const notification = getNotificationFromContext(listService); - if (!notification) { - return false; - } - commandService.executeCommand('notifications.showList'); - let notificationIndex: number | undefined; - let length: number | undefined; - const list = listService.lastFocusedList; - if (list instanceof WorkbenchList) { - notificationIndex = list.indexOf(notification); - length = list.length; - } - if (notificationIndex === undefined) { - return false; - } - - function focusList(): void { - commandService.executeCommand('notifications.showList'); - if (list && notificationIndex !== undefined) { - list.domFocus(); - try { - list.setFocus([notificationIndex]); - } catch { } - } - } - const message = notification.message.original.toString(); - if (!message) { - return false; - } - notification.onDidClose(() => accessibleViewService.next()); - accessibleViewService.show({ - id: AccessibleViewProviderId.Notification, - provideContent: () => { - return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); - }, - onClose(): void { - focusList(); - }, - next(): void { - if (!list) { - return; - } - focusList(); - list.focusNext(); - alertFocusChange(notificationIndex, length, 'next'); - renderAccessibleView(); - }, - previous(): void { - if (!list) { - return; - } - focusList(); - list.focusPrevious(); - alertFocusChange(notificationIndex, length, 'previous'); - renderAccessibleView(); - }, - verbositySettingKey: AccessibilityVerbositySettingId.Notification, - options: { type: AccessibleViewType.View }, - actions: getActionsFromNotification(notification, accessibilitySignalService) - }); - return true; - } - return renderAccessibleView(); - }, NotificationFocusedContext)); - } -} - -function getActionsFromNotification(notification: INotificationViewItem, accessibilitySignalService: IAccessibilitySignalService): IAction[] | undefined { - let actions = undefined; - if (notification.actions) { - actions = []; - if (notification.actions.primary) { - actions.push(...notification.actions.primary); - } - if (notification.actions.secondary) { - actions.push(...notification.actions.secondary); - } - } - if (actions) { - for (const action of actions) { - action.class = ThemeIcon.asClassName(Codicon.bell); - const initialAction = action.run; - action.run = () => { - initialAction(); - notification.close(); - }; - } - } - const manageExtension = actions?.find(a => a.label.includes('Manage Extension')); - if (manageExtension) { - manageExtension.class = ThemeIcon.asClassName(Codicon.gear); - } - if (actions) { - actions.push({ - id: 'clearNotification', label: localize('clearNotification', "Clear Notification"), tooltip: localize('clearNotification', "Clear Notification"), run: () => { - notification.close(); - accessibilitySignalService.playSignal(AccessibilitySignal.clear); - }, enabled: true, class: ThemeIcon.asClassName(Codicon.clearAll) - }); - } - return actions; -} - -export function alertFocusChange(index: number | undefined, length: number | undefined, type: 'next' | 'previous'): void { - if (index === undefined || length === undefined) { - return; - } - const number = index + 1; - - if (type === 'next' && number + 1 <= length) { - alert(`Focused ${number + 1} of ${length}`); - } else if (type === 'previous' && number - 1 > 0) { - alert(`Focused ${number - 1} of ${length}`); - } - return; -} - -export class InlineCompletionsAccessibleViewContribution extends Disposable { - static ID: 'inlineCompletionsAccessibleViewContribution'; - private _options: IAccessibleViewOptions = { type: AccessibleViewType.View }; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(95, 'inline-completions', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); - const show = () => { - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!editor) { - return false; - } - const model = InlineCompletionsController.get(editor)?.model.get(); - const state = model?.state.get(); - if (!model || !state) { - return false; - } - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - const ghostText = state.primaryGhostText.renderForScreenReader(lineText); - if (!ghostText) { - return false; - } - this._options.language = editor.getModel()?.getLanguageId() ?? undefined; - accessibleViewService.show({ - id: AccessibleViewProviderId.InlineCompletions, - verbositySettingKey: AccessibilityVerbositySettingId.InlineCompletions, - provideContent() { return lineText + ghostText; }, - onClose() { - model.stop(); - editor.focus(); - }, - next() { - model.next(); - setTimeout(() => show(), 50); - }, - previous() { - model.previous(); - setTimeout(() => show(), 50); - }, - options: this._options - }); - return true; - }; ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); - return show(); - })); - } -} - diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index b64f3dc5e62..7bd44b225c2 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -9,7 +9,6 @@ import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; @@ -25,7 +24,8 @@ import { IModelService } from 'vs/editor/common/services/model'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol } from 'vs/platform/accessibility/browser/accessibleView'; +import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; @@ -33,14 +33,15 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewDelegate, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { resolveContentAndKeybindingItems } from 'vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; @@ -50,85 +51,7 @@ const enum DIMENSIONS { MAX_WIDTH = 600 } -export interface IAccessibleContentProvider { - id: AccessibleViewProviderId; - verbositySettingKey: AccessibilityVerbositySettingId; - options: IAccessibleViewOptions; - /** - * Note that a Codicon class should be provided for each action. - * If not, a default will be used. - */ - actions?: IAction[]; - provideContent(): string; - onClose(): void; - onKeyDown?(e: IKeyboardEvent): void; - previous?(): void; - next?(): void; - /** - * When the language is markdown, this is provided by default. - */ - getSymbols?(): IAccessibleViewSymbol[]; - /** - * Note that this will only take effect if the provider has an ID. - */ - onDidRequestClearLastProvider?: Event; -} - -export const IAccessibleViewService = createDecorator('accessibleViewService'); - -export interface IAccessibleViewService { - readonly _serviceBrand: undefined; - show(provider: IAccessibleContentProvider, position?: Position): void; - showLastProvider(id: AccessibleViewProviderId): void; - showAccessibleViewHelp(): void; - next(): void; - previous(): void; - goToSymbol(): void; - disableHint(): void; - getPosition(id: AccessibleViewProviderId): Position | undefined; - setPosition(position: Position, reveal?: boolean): void; - getLastPosition(): Position | undefined; - /** - * If the setting is enabled, provides the open accessible view hint as a localized string. - * @param verbositySettingKey The setting key for the verbosity of the feature - */ - getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null; - getCodeBlockContext(): ICodeBlockActionContext | undefined; -} - -export const enum AccessibleViewType { - Help = 'help', - View = 'view' -} - -export const enum NavigationType { - Previous = 'previous', - Next = 'next' -} - -export interface IAccessibleViewOptions { - readMoreUrl?: string; - /** - * Defaults to markdown - */ - language?: string; - type: AccessibleViewType; - /** - * By default, places the cursor on the top line of the accessible view. - * If set to 'initial-bottom', places the cursor on the bottom line of the accessible view and preserves it henceforth. - * If set to 'bottom', places the cursor on the bottom line of the accessible view. - */ - position?: 'bottom' | 'initial-bottom'; - /** - * @returns a string that will be used as the content of the help dialog - * instead of the one provided by default. - */ - customHelp?: () => string; - /** - * If this provider might want to request to be shown again, provide an ID. - */ - id?: AccessibleViewProviderId; -} +export type AccesibleViewContentProvider = AccessibleContentProvider | ExtensionContentProvider; interface ICodeBlock { startLine: number; @@ -149,17 +72,24 @@ export class AccessibleView extends Disposable { private _accessibleViewCurrentProviderId: IContextKey; private _accessibleViewInCodeBlock: IContextKey; private _accessibleViewContainsCodeBlocks: IContextKey; + private _hasUnassignedKeybindings: IContextKey; + private _hasAssignedKeybindings: IContextKey; + private _codeBlocks?: ICodeBlock[]; + private _inQuickPick: boolean = false; get editorWidget() { return this._editorWidget; } private _container: HTMLElement; private _title: HTMLElement; private readonly _toolbar: WorkbenchToolBar; - private _currentProvider: IAccessibleContentProvider | undefined; + private _currentProvider: AccesibleViewContentProvider | undefined; private _currentContent: string | undefined; - private _lastProvider: IAccessibleContentProvider | undefined; + private _lastProvider: AccesibleViewContentProvider | undefined; + + private _viewContainer: HTMLElement | undefined; + constructor( @IOpenerService private readonly _openerService: IOpenerService, @@ -173,7 +103,9 @@ export class AccessibleView extends Disposable { @ILayoutService private readonly _layoutService: ILayoutService, @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, - @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService + @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService, + @IStorageService private readonly _storageService: IStorageService, + @IQuickInputService private readonly _quickInputService: IQuickInputService ) { super(); @@ -186,6 +118,8 @@ export class AccessibleView extends Disposable { this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService); this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService); this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService); + this._hasUnassignedKeybindings = accessibleViewHasUnassignedKeybindings.bindTo(this._contextKeyService); + this._hasAssignedKeybindings = accessibleViewHasAssignedKeybindings.bindTo(this._contextKeyService); this._container = document.createElement('div'); this._container.classList.add('accessible-view'); @@ -214,6 +148,7 @@ export class AccessibleView extends Disposable { lineDecorationsWidth: 6, dragAndDrop: false, cursorWidth: 1, + wordWrap: 'off', wrappingStrategy: 'advanced', wrappingIndent: 'none', padding: { top: 2, bottom: 2 }, @@ -230,7 +165,7 @@ export class AccessibleView extends Disposable { } })); this._register(this._configurationService.onDidChangeConfiguration(e => { - if (this._currentProvider && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) { + if (this._currentProvider instanceof AccessibleContentProvider && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) { if (this._accessiblityHelpIsShown.get()) { this.show(this._currentProvider); } @@ -261,6 +196,8 @@ export class AccessibleView extends Disposable { this._accessibleViewVerbosityEnabled.reset(); this._accessibleViewGoToSymbolSupported.reset(); this._accessibleViewCurrentProviderId.reset(); + this._hasAssignedKeybindings.reset(); + this._hasUnassignedKeybindings.reset(); } getPosition(id?: AccessibleViewProviderId): Position | undefined { @@ -270,11 +207,17 @@ export class AccessibleView extends Disposable { return this._editorWidget.getPosition() || undefined; } - setPosition(position: Position, reveal?: boolean): void { + setPosition(position: Position, reveal?: boolean, select?: boolean): void { this._editorWidget.setPosition(position); if (reveal) { this._editorWidget.revealPosition(position); } + if (select) { + const lineLength = this._editorWidget.getModel()?.getLineLength(position.lineNumber) ?? 0; + if (lineLength) { + this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: lineLength + 1 }); + } + } } getCodeBlockContext(): ICodeBlockActionContext | undefined { @@ -290,6 +233,24 @@ export class AccessibleView extends Disposable { return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined }; } + navigateToCodeBlock(type: 'next' | 'previous'): void { + const position = this._editorWidget.getPosition(); + if (!this._codeBlocks?.length || !position) { + return; + } + let codeBlock; + const codeBlocks = this._codeBlocks.slice(); + if (type === 'previous') { + codeBlock = codeBlocks.reverse().find(c => c.endLine < position.lineNumber); + } else { + codeBlock = codeBlocks.find(c => c.startLine > position.lineNumber); + } + if (!codeBlock) { + return; + } + this.setPosition(new Position(codeBlock.startLine, 1), true); + } + showLastProvider(id: AccessibleViewProviderId): void { if (!this._lastProvider || this._lastProvider.options.id !== id) { return; @@ -297,20 +258,23 @@ export class AccessibleView extends Disposable { this.show(this._lastProvider); } - show(provider?: IAccessibleContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: Position): void { + show(provider?: AccesibleViewContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: Position): void { provider = provider ?? this._currentProvider; if (!provider) { return; } + provider.onOpen?.(); const delegate: IContextViewDelegate = { getAnchor: () => { return { x: (getActiveWindow().innerWidth / 2) - ((Math.min(this._layoutService.activeContainerDimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.activeContainerOffset.quickPickTop }; }, render: (container) => { - container.classList.add('accessible-view-container'); + this._viewContainer = container; + this._viewContainer.classList.add('accessible-view-container'); return this._render(provider, container, showAccessibleViewHelp); }, onHide: () => { if (!showAccessibleViewHelp) { this._updateLastProvider(); + this._currentProvider?.dispose(); this._currentProvider = undefined; this._resetContextKeys(); } @@ -320,17 +284,17 @@ export class AccessibleView extends Disposable { if (position) { // Context view takes time to show up, so we need to wait for it to show up before we can set the position - setTimeout(() => { + queueMicrotask(() => { this._editorWidget.revealLine(position.lineNumber); this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column }); - }, 10); + }); } if (symbol && this._currentProvider) { this.showSymbol(this._currentProvider, symbol); } - if (provider.onDidRequestClearLastProvider) { - this._register(provider.onDidRequestClearLastProvider((id) => { + if (provider instanceof AccessibleContentProvider && provider.onDidRequestClearLastProvider) { + this._register(provider.onDidRequestClearLastProvider((id: string) => { if (this._lastProvider?.options.id === id) { this._lastProvider = undefined; } @@ -343,20 +307,37 @@ export class AccessibleView extends Disposable { if (provider.id === AccessibleViewProviderId.Chat) { this._register(this._codeBlockContextProviderService.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView')); } + if (provider instanceof ExtensionContentProvider) { + this._storageService.store(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${provider.id}`, true, StorageScope.APPLICATION, StorageTarget.USER); + } + if (provider.onDidChangeContent) { + this._register(provider.onDidChangeContent(() => { + if (this._viewContainer) { this._render(provider, this._viewContainer, showAccessibleViewHelp); } + })); + } } previous(): void { - if (!this._currentProvider) { + const newContent = this._currentProvider?.providePreviousContent?.(); + if (!this._currentProvider || !this._viewContainer || !newContent) { return; } - this._currentProvider.previous?.(); + this._render(this._currentProvider, this._viewContainer, undefined, newContent); } next(): void { - if (!this._currentProvider) { + const newContent = this._currentProvider?.provideNextContent?.(); + if (!this._currentProvider || !this._viewContainer || !newContent) { return; } - this._currentProvider.next?.(); + this._render(this._currentProvider, this._viewContainer, undefined, newContent); + } + + private _verbosityEnabled(): boolean { + if (!this._currentProvider) { + return false; + } + return this._currentProvider instanceof AccessibleContentProvider ? this._configurationService.getValue(this._currentProvider.verbositySettingKey) === true : this._storageService.getBoolean(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${this._currentProvider.id}`, StorageScope.APPLICATION, false); } goToSymbol(): void { @@ -366,7 +347,10 @@ export class AccessibleView extends Disposable { this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider); } - calculateCodeBlocks(markdown: string): void { + calculateCodeBlocks(markdown?: string): void { + if (!markdown) { + return; + } if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { return; } @@ -385,7 +369,7 @@ export class AccessibleView extends Disposable { inBlock = true; startLine = i + 1; languageId = line.substring(3).trim(); - } else if (inBlock && line.startsWith('```')) { + } else if (inBlock && line.endsWith('```')) { inBlock = false; const endLine = i; const code = lines.slice(startLine, endLine).join('\n'); @@ -396,14 +380,15 @@ export class AccessibleView extends Disposable { } getSymbols(): IAccessibleViewSymbol[] | undefined { - if (!this._currentProvider || !this._currentContent) { + const provider = this._currentProvider instanceof AccessibleContentProvider ? this._currentProvider : undefined; + if (!this._currentContent || !provider) { return; } - const symbols: IAccessibleViewSymbol[] = this._currentProvider.getSymbols?.() || []; + const symbols: IAccessibleViewSymbol[] = provider.getSymbols?.() || []; if (symbols?.length) { return symbols; } - if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') { + if (provider.options.language && provider.options.language !== 'markdown') { // Symbols haven't been provided and we cannot parse this language return; } @@ -415,6 +400,42 @@ export class AccessibleView extends Disposable { return symbols.length ? symbols : undefined; } + openHelpLink(): void { + if (!this._currentProvider?.options.readMoreUrl) { + return; + } + this._openerService.open(URI.parse(this._currentProvider.options.readMoreUrl)); + } + + configureKeybindings(unassigned: boolean): void { + this._inQuickPick = true; + const provider = this._updateLastProvider(); + const items = unassigned ? provider?.options?.configureKeybindingItems : provider?.options?.configuredKeybindingItems; + if (!items) { + return; + } + const quickPick: IQuickPick = this._quickInputService.createQuickPick(); + this._register(quickPick); + quickPick.items = items; + quickPick.title = localize('keybindings', 'Configure keybindings'); + quickPick.placeholder = localize('selectKeybinding', 'Select a command ID to configure a keybinding for it'); + quickPick.show(); + quickPick.onDidAccept(async () => { + const item = quickPick.selectedItems[0]; + if (item) { + await this._commandService.executeCommand('workbench.action.openGlobalKeybindings', item.id); + } + quickPick.dispose(); + }); + quickPick.onDidHide(() => { + if (!quickPick.selectedItems.length && provider) { + this.show(provider); + } + quickPick.dispose(); + this._inQuickPick = false; + }); + } + private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void { let firstListItem: string | undefined; for (const token of tokens) { @@ -444,7 +465,7 @@ export class AccessibleView extends Disposable { } } - showSymbol(provider: IAccessibleContentProvider, symbol: IAccessibleViewSymbol): void { + showSymbol(provider: AccesibleViewContentProvider, symbol: IAccessibleViewSymbol): void { if (!this._currentContent) { return; } @@ -471,14 +492,14 @@ export class AccessibleView extends Disposable { } disableHint(): void { - if (!this._currentProvider) { + if (!(this._currentProvider instanceof AccessibleContentProvider)) { return; } this._configurationService.updateValue(this._currentProvider?.verbositySettingKey, false); alert(localize('disableAccessibilityHelp', '{0} accessibility verbosity is now disabled', this._currentProvider.verbositySettingKey)); } - private _updateContextKeys(provider: IAccessibleContentProvider, shown: boolean): void { + private _updateContextKeys(provider: AccesibleViewContentProvider, shown: boolean): void { if (provider.options.type === AccessibleViewType.Help) { this._accessiblityHelpIsShown.set(shown); this._accessibleViewIsShown.reset(); @@ -486,49 +507,55 @@ export class AccessibleView extends Disposable { this._accessibleViewIsShown.set(shown); this._accessiblityHelpIsShown.reset(); } - if (provider.next && provider.previous) { - this._accessibleViewSupportsNavigation.set(true); - } else { - this._accessibleViewSupportsNavigation.reset(); - } - const verbosityEnabled: boolean = this._configurationService.getValue(provider.verbositySettingKey); - this._accessibleViewVerbosityEnabled.set(verbosityEnabled); + this._accessibleViewSupportsNavigation.set(provider.provideNextContent !== undefined || provider.providePreviousContent !== undefined); + this._accessibleViewVerbosityEnabled.set(this._verbosityEnabled()); this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false); } - private _render(provider: IAccessibleContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { - this._currentProvider = provider; - this._accessibleViewCurrentProviderId.set(provider.id); - const value = this._configurationService.getValue(provider.verbositySettingKey); - const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : ''; - let disableHelpHint = ''; - if (provider.options.type === AccessibleViewType.Help && !!value) { - disableHelpHint = this._getDisableVerbosityHint(provider.verbositySettingKey); + private _updateContent(provider: AccesibleViewContentProvider, updatedContent?: string): void { + let content = updatedContent ?? provider.provideContent(); + if (provider.options.type === AccessibleViewType.View) { + this._currentContent = content; + this._hasUnassignedKeybindings.reset(); + this._hasAssignedKeybindings.reset(); + return; } - const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized(); - let message = ''; - if (provider.options.type === AccessibleViewType.Help) { - const turnOnMessage = ( - isMacintosh - ? AccessibilityHelpNLS.changeConfigToOnMac - : AccessibilityHelpNLS.changeConfigToOnWinLinux - ); - if (accessibilitySupport && provider.verbositySettingKey === AccessibilityVerbositySettingId.Editor) { - message = AccessibilityHelpNLS.auto_on; - message += '\n'; - } else if (!accessibilitySupport) { - message = AccessibilityHelpNLS.auto_off + '\n' + turnOnMessage; - message += '\n'; + const readMoreLinkHint = this._readMoreHint(provider); + const disableHelpHint = this._disableVerbosityHint(provider); + const screenReaderModeHint = this._screenReaderModeHint(provider); + const exitThisDialogHint = this._exitDialogHint(provider); + let configureKbHint = ''; + let configureAssignedKbHint = ''; + const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, screenReaderModeHint + content + readMoreLinkHint + disableHelpHint + exitThisDialogHint); + if (resolvedContent) { + content = resolvedContent.content.value; + if (resolvedContent.configureKeybindingItems) { + provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems; + this._hasUnassignedKeybindings.set(true); + configureKbHint = this._configureUnassignedKbHint(); + } else { + this._hasAssignedKeybindings.reset(); + } + if (resolvedContent.configuredKeybindingItems) { + provider.options.configuredKeybindingItems = resolvedContent.configuredKeybindingItems; + this._hasAssignedKeybindings.set(true); + configureAssignedKbHint = this._configureAssignedKbHint(); + } else { + this._hasAssignedKeybindings.reset(); } } - const verbose = this._configurationService.getValue(provider.verbositySettingKey); - const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; - const newContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; - this.calculateCodeBlocks(newContent); - this._currentContent = newContent; + this._currentContent = content + configureKbHint + configureAssignedKbHint; + } + + private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean, updatedContent?: string): IDisposable { + this._currentProvider = provider; + this._accessibleViewCurrentProviderId.set(provider.id); + const verbose = this._verbosityEnabled(); + this._updateContent(provider, updatedContent); + this.calculateCodeBlocks(this._currentContent); this._updateContextKeys(provider, true); const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus(); - this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { + this._getTextModel(URI.from({ path: `accessible-view-${provider.id}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { if (!model) { return; } @@ -540,8 +567,7 @@ export class AccessibleView extends Disposable { model.setLanguage(provider.options.language ?? 'markdown'); container.appendChild(this._container); let actionsHint = ''; - const verbose = this._configurationService.getValue(provider.verbositySettingKey); - const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || this._currentProvider?.actions; + const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || provider.actions?.length; if (verbose && !showAccessibleViewHelp && hasActions) { actionsHint = provider.options.position ? localize('ariaAccessibleViewActionsBottom', 'Explore actions such as disabling this hint (Shift+Tab), use Escape to exit this dialog.') : localize('ariaAccessibleViewActions', 'Explore actions such as disabling this hint (Shift+Tab).'); } @@ -572,15 +598,19 @@ export class AccessibleView extends Disposable { } } }); - this._updateToolbar(provider.actions, provider.options.type); + this._updateToolbar(this._currentProvider.actions, provider.options.type); - const hide = (e: KeyboardEvent | IKeyboardEvent): void => { - provider.onClose(); - e.stopPropagation(); + const hide = (e?: KeyboardEvent | IKeyboardEvent): void => { + if (!this._inQuickPick) { + provider.onClose(); + } + e?.stopPropagation(); this._contextViewService.hideContextView(); this._updateContextKeys(provider, false); this._lastProvider = undefined; this._currentContent = undefined; + this._currentProvider?.dispose(); + this._currentProvider = undefined; }; const disposableStore = new DisposableStore(); disposableStore.add(this._editorWidget.onKeyDown((e) => { @@ -595,7 +625,9 @@ export class AccessibleView extends Disposable { e.preventDefault(); e.stopPropagation(); } - provider.onKeyDown?.(e); + if (provider instanceof AccessibleContentProvider) { + provider.onKeyDown?.(e); + } })); disposableStore.add(addDisposableListener(this._toolbar.getElement(), EventType.KEY_DOWN, (e: KeyboardEvent) => { const keyboardEvent = new StandardKeyboardEvent(e); @@ -605,7 +637,7 @@ export class AccessibleView extends Disposable { })); disposableStore.add(this._editorWidget.onDidBlurEditorWidget(() => { if (!isActiveElement(this._toolbar.getElement())) { - this._contextViewService.hideContextView(); + hide(); } })); disposableStore.add(this._editorWidget.onDidContentSizeChange(() => this._layout())); @@ -649,17 +681,38 @@ export class AccessibleView extends Disposable { if (!this._currentProvider) { return false; } - return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || !!this._currentProvider.getSymbols?.(); + return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AccessibleContentProvider && !!this._currentProvider.getSymbols?.()); } - private _updateLastProvider(): IAccessibleContentProvider | undefined { - if (!this._currentProvider) { + private _updateLastProvider(): AccesibleViewContentProvider | undefined { + const provider = this._currentProvider; + if (!provider) { return; } - const lastProvider = Object.assign({}, this._currentProvider); - lastProvider.provideContent = this._currentProvider.provideContent.bind(lastProvider); - lastProvider.options = Object.assign({}, this._currentProvider.options); - lastProvider.verbositySettingKey = this._currentProvider.verbositySettingKey; + const lastProvider = provider instanceof AccessibleContentProvider ? new AccessibleContentProvider( + provider.id, + provider.options, + provider.provideContent.bind(provider), + provider.onClose.bind(provider), + provider.verbositySettingKey, + provider.onOpen?.bind(provider), + provider.actions, + provider.provideNextContent?.bind(provider), + provider.providePreviousContent?.bind(provider), + provider.onDidChangeContent?.bind(provider), + provider.onKeyDown?.bind(provider), + provider.getSymbols?.bind(provider), + ) : new ExtensionContentProvider( + provider.id, + provider.options, + provider.provideContent.bind(provider), + provider.onClose.bind(provider), + provider.onOpen?.bind(provider), + provider.provideNextContent?.bind(provider), + provider.providePreviousContent?.bind(provider), + provider.actions, + provider.onDidChangeContent?.bind(provider), + ); return lastProvider; } @@ -668,22 +721,41 @@ export class AccessibleView extends Disposable { if (!lastProvider) { return; } - - const accessibleViewHelpProvider: IAccessibleContentProvider = { - id: lastProvider.id, - provideContent: () => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._getAccessibleViewHelpDialogContent(this._goToSymbolsSupported()), - onClose: () => this.show(lastProvider), - options: { type: AccessibleViewType.Help }, - verbositySettingKey: lastProvider.verbositySettingKey - }; + let accessibleViewHelpProvider; + if (lastProvider instanceof AccessibleContentProvider) { + accessibleViewHelpProvider = new AccessibleContentProvider( + lastProvider.id as AccessibleViewProviderId, + { type: AccessibleViewType.Help }, + () => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()), + () => { + this._contextViewService.hideContextView(); + // HACK: Delay to allow the context view to hide #207638 + queueMicrotask(() => this.show(lastProvider)); + }, + lastProvider.verbositySettingKey as AccessibilityVerbositySettingId + ); + } else { + accessibleViewHelpProvider = new ExtensionContentProvider( + lastProvider.id as AccessibleViewProviderId, + { type: AccessibleViewType.Help }, + () => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()), + () => { + this._contextViewService.hideContextView(); + // HACK: Delay to allow the context view to hide #207638 + queueMicrotask(() => this.show(lastProvider)); + }, + ); + } this._contextViewService.hideContextView(); // HACK: Delay to allow the context view to hide #186514 - setTimeout(() => this.show(accessibleViewHelpProvider, undefined, true), 100); + if (accessibleViewHelpProvider) { + queueMicrotask(() => this.show(accessibleViewHelpProvider, undefined, true)); + } } - private _getAccessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { - const navigationHint = this._getNavigationHint(); - const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); + private _accessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { + const navigationHint = this._navigationHint(); + const goToSymbolHint = this._goToSymbolHint(providerHasSymbols); const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab)."); const chatHints = this._getChatHints(); @@ -707,66 +779,65 @@ export class AccessibleView extends Disposable { if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { return; } - let hint = ''; - const insertAtCursorKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertCodeBlock')?.getAriaLabel(); - const insertIntoNewFileKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertIntoNewFile')?.getAriaLabel(); - const runInTerminalKb = this._keybindingService.lookupKeybinding('workbench.action.chat.runInTerminal')?.getAriaLabel(); - - if (insertAtCursorKb) { - hint += localize('insertAtCursor', " - Insert the code block at the cursor ({0}).\n", insertAtCursorKb); - } else { - hint += localize('insertAtCursorNoKb', " - Insert the code block at the cursor by configuring a keybinding for the Chat: Insert Code Block command.\n"); - } - if (insertIntoNewFileKb) { - hint += localize('insertIntoNewFile', " - Insert the code block into a new file ({0}).\n", insertIntoNewFileKb); - } else { - hint += localize('insertIntoNewFileNoKb', " - Insert the code block into a new file by configuring a keybinding for the Chat: Insert into New File command.\n"); - } - if (runInTerminalKb) { - hint += localize('runInTerminal', " - Run the code block in the terminal ({0}).\n", runInTerminalKb); - } else { - hint += localize('runInTerminalNoKb', " - Run the coe block in the terminal by configuring a keybinding for the Chat: Insert into Terminal command.\n"); - } - - return hint; + return [localize('insertAtCursor', " - Insert the code block at the cursor{0}.", ''), + localize('insertIntoNewFile', " - Insert the code block into a new file{0}.", ''), + localize('runInTerminal', " - Run the code block in the terminal{0}.\n", '')].join('\n'); } - private _getNavigationHint(): string { - let hint = ''; - const nextKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowNext)?.getAriaLabel(); - const previousKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowPrevious)?.getAriaLabel(); - if (nextKeybinding && previousKeybinding) { - hint = localize('accessibleViewNextPreviousHint', "Show the next ({0}) or previous ({1}) item.", nextKeybinding, previousKeybinding); - } else { - hint = localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands."); - } - return hint; - } - private _getDisableVerbosityHint(verbositySettingKey: AccessibilityVerbositySettingId): string { - if (!this._configurationService.getValue(verbositySettingKey)) { - return ''; - } - let hint = ''; - const disableKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.DisableVerbosityHint, this._contextKeyService)?.getAriaLabel(); - if (disableKeybinding) { - hint = localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature ({0}).", disableKeybinding); - } else { - hint = localize('accessibleViewDisableHintNoKb', "\n\nAdd a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.s"); - } - return hint; + private _navigationHint(): string { + return localize('accessibleViewNextPreviousHint', "Show the next item{0} or previous item{1}.", ``); } - private _getGoToSymbolHint(providerHasSymbols?: boolean): string { - const goToSymbolKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.GoToSymbol)?.getAriaLabel(); - let goToSymbolHint = ''; - if (providerHasSymbols) { - if (goToSymbolKb) { - goToSymbolHint = localize('goToSymbolHint', 'Go to a symbol ({0}).', goToSymbolKb); - } else { - goToSymbolHint = localize('goToSymbolHintNoKb', 'To go to a symbol, configure a keybinding for the command Go To Symbol in Accessible View'); - } + private _disableVerbosityHint(provider: AccesibleViewContentProvider): string { + if (provider.options.type === AccessibleViewType.Help && this._verbosityEnabled()) { + return localize('acessibleViewDisableHint', "\nDisable accessibility verbosity for this feature{0}.", ``); } - return goToSymbolHint; + return ''; + } + + private _goToSymbolHint(providerHasSymbols?: boolean): string | undefined { + if (!providerHasSymbols) { + return; + } + return localize('goToSymbolHint', 'Go to a symbol{0}.', ``); + } + + private _configureUnassignedKbHint(): string { + const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel(); + const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Unassigned Keybindings.'; + return localize('configureKb', '\nConfigure keybindings for commands that lack them {0}.', keybindingToConfigureQuickPick); + } + + private _configureAssignedKbHint(): string { + const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings)?.getAriaLabel(); + const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Assigned Keybindings.'; + return localize('configureKbAssigned', '\nConfigure keybindings for commands that already have assignments {0}.', keybindingToConfigureQuickPick); + } + + private _screenReaderModeHint(provider: AccesibleViewContentProvider): string { + const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized(); + let screenReaderModeHint = ''; + const turnOnMessage = ( + isMacintosh + ? AccessibilityHelpNLS.changeConfigToOnMac + : AccessibilityHelpNLS.changeConfigToOnWinLinux + ); + if (accessibilitySupport && provider.id === AccessibleViewProviderId.Editor) { + screenReaderModeHint = AccessibilityHelpNLS.auto_on; + screenReaderModeHint += '\n'; + } else if (!accessibilitySupport) { + screenReaderModeHint = AccessibilityHelpNLS.auto_off + '\n' + turnOnMessage; + screenReaderModeHint += '\n'; + } + return screenReaderModeHint; + } + + private _exitDialogHint(provider: AccesibleViewContentProvider): string { + return this._verbosityEnabled() && !provider.options.position ? localize('exit', '\nExit this dialog (Escape).') : ''; + } + + private _readMoreHint(provider: AccesibleViewContentProvider): string { + return provider.options.readMoreUrl ? localize("openDoc", "\nOpen a browser window with more information related to accessibility{0}.", ``) : ''; } } @@ -782,12 +853,18 @@ export class AccessibleViewService extends Disposable implements IAccessibleView super(); } - show(provider: IAccessibleContentProvider, position?: Position): void { + show(provider: AccesibleViewContentProvider, position?: Position): void { if (!this._accessibleView) { this._accessibleView = this._register(this._instantiationService.createInstance(AccessibleView)); } this._accessibleView.show(provider, undefined, undefined, position); } + configureKeybindings(unassigned: boolean): void { + this._accessibleView?.configureKeybindings(unassigned); + } + openHelpLink(): void { + this._accessibleView?.openHelpLink(); + } showLastProvider(id: AccessibleViewProviderId): void { this._accessibleView?.showLastProvider(id); } @@ -826,23 +903,22 @@ export class AccessibleViewService extends Disposable implements IAccessibleView const lastLine = this._accessibleView?.editorWidget.getModel()?.getLineCount(); return lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined; } - setPosition(position: Position, reveal?: boolean): void { - const editorWidget = this._accessibleView?.editorWidget; - editorWidget?.setPosition(position); - if (reveal) { - editorWidget?.revealLine(position.lineNumber); - } + setPosition(position: Position, reveal?: boolean, select?: boolean): void { + this._accessibleView?.setPosition(position, reveal, select); } getCodeBlockContext(): ICodeBlockActionContext | undefined { return this._accessibleView?.getCodeBlockContext(); } + navigateToCodeBlock(type: 'next' | 'previous'): void { + this._accessibleView?.navigateToCodeBlock(type); + } } class AccessibleViewSymbolQuickPick { constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) { } - show(provider: IAccessibleContentProvider): void { + show(provider: AccesibleViewContentProvider): void { const quickPick = this._quickInputService.createQuickPick(); quickPick.placeholder = localize('accessibleViewSymbolQuickPickPlaceholder', "Type to search symbols"); quickPick.title = localize('accessibleViewSymbolQuickPickTitle', "Go to Symbol Accessible View"); @@ -873,12 +949,6 @@ class AccessibleViewSymbolQuickPick { } } -export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { - markdownToParse?: string; - firstListItem?: string; - lineNumber?: number; - endLineNumber?: number; -} function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean { if (!configurationService.getValue(AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress)) { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index fa6f0f8d633..67df062dfe0 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -11,8 +11,8 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; @@ -52,6 +52,56 @@ class AccessibleViewNextAction extends Action2 { registerAction2(AccessibleViewNextAction); +class AccessibleViewNextCodeBlockAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.NextCodeBlock, + precondition: ContextKeyExpr.and(accessibleViewContainsCodeBlocks, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Chat)), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, }, + weight: KeybindingWeight.WorkbenchContrib, + }, + icon: Codicon.arrowRight, + menu: + { + ...accessibleViewMenu, + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewContainsCodeBlocks), + }, + title: localize('editor.action.accessibleViewNextCodeBlock', "Accessible View: Next Code Block") + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IAccessibleViewService).navigateToCodeBlock('next'); + } +} +registerAction2(AccessibleViewNextCodeBlockAction); + + +class AccessibleViewPreviousCodeBlockAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.PreviousCodeBlock, + precondition: ContextKeyExpr.and(accessibleViewContainsCodeBlocks, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Chat)), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, }, + weight: KeybindingWeight.WorkbenchContrib, + }, + icon: Codicon.arrowLeft, + menu: { + ...accessibleViewMenu, + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewContainsCodeBlocks), + }, + title: localize('editor.action.accessibleViewPreviousCodeBlock', "Accessible View: Previous Code Block") + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IAccessibleViewService).navigateToCodeBlock('previous'); + } +} +registerAction2(AccessibleViewPreviousCodeBlockAction); + class AccessibleViewPreviousAction extends Action2 { constructor() { super({ @@ -178,6 +228,79 @@ class AccessibleViewDisableHintAction extends Action2 { } registerAction2(AccessibleViewDisableHintAction); +class AccessibilityHelpConfigureKeybindingsAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpConfigureKeybindings, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewHasUnassignedKeybindings), + icon: Codicon.key, + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyK, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: [ + { + id: MenuId.AccessibleView, + group: 'navigation', + order: 3, + when: accessibleViewHasUnassignedKeybindings, + } + ], + title: localize('editor.action.accessibilityHelpConfigureUnassignedKeybindings', "Accessibility Help Configure Unassigned Keybindings") + }); + } + async run(accessor: ServicesAccessor): Promise { + await accessor.get(IAccessibleViewService).configureKeybindings(true); + } +} +registerAction2(AccessibilityHelpConfigureKeybindingsAction); + +class AccessibilityHelpConfigureAssignedKeybindingsAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewHasAssignedKeybindings), + icon: Codicon.key, + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyA, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: [ + { + id: MenuId.AccessibleView, + group: 'navigation', + order: 4, + when: accessibleViewHasAssignedKeybindings, + } + ], + title: localize('editor.action.accessibilityHelpConfigureAssignedKeybindings', "Accessibility Help Configure Assigned Keybindings") + }); + } + async run(accessor: ServicesAccessor): Promise { + await accessor.get(IAccessibleViewService).configureKeybindings(false); + } +} +registerAction2(AccessibilityHelpConfigureAssignedKeybindingsAction); + + +class AccessibilityHelpOpenHelpLinkAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpOpenHelpLink, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown), + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyH, + weight: KeybindingWeight.WorkbenchContrib + }, + title: localize('editor.action.accessibilityHelpOpenHelpLink', "Accessibility Help Open Help Link") + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IAccessibleViewService).openHelpLink(); + } +} +registerAction2(AccessibilityHelpOpenHelpLinkAction); + class AccessibleViewAcceptInlineCompletionAction extends Action2 { constructor() { super({ @@ -217,3 +340,4 @@ class AccessibleViewAcceptInlineCompletionAction extends Action2 { } } registerAction2(AccessibleViewAcceptInlineCompletionAction); + diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts new file mode 100644 index 00000000000..bc026ec30af --- /dev/null +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export class AccesibleViewHelpContribution extends Disposable { + static ID: 'accesibleViewHelpContribution'; + constructor() { + super(); + this._register(AccessibilityHelpAction.addImplementation(115, 'accessible-view-help', accessor => { + accessor.get(IAccessibleViewService).showAccessibleViewHelp(); + return true; + }, accessibleViewIsShown)); + } +} + +export class AccesibleViewContributions extends Disposable { + static ID: 'accesibleViewContributions'; + constructor() { + super(); + AccessibleViewRegistry.getImplementations().forEach(impl => { + const implementation = (accessor: ServicesAccessor) => { + const provider: AccessibleContentProvider | ExtensionContentProvider | undefined = impl.getProvider(accessor); + if (!provider) { + return false; + } + try { + accessor.get(IAccessibleViewService).show(provider); + return true; + } catch { + provider.dispose(); + return false; + } + }; + if (impl.type === AccessibleViewType.View) { + this._register(AccessibleViewAction.addImplementation(impl.priority, impl.name, implementation, impl.when)); + } else { + this._register(AccessibilityHelpAction.addImplementation(impl.priority, impl.name, implementation, impl.when)); + } + }); + } +} diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts new file mode 100644 index 00000000000..107f60f28ce --- /dev/null +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.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 { MarkdownString } from 'vs/base/common/htmlContent'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; + +export function resolveContentAndKeybindingItems(keybindingService: IKeybindingService, value?: string): { content: MarkdownString; configureKeybindingItems: IPickerQuickAccessItem[] | undefined; configuredKeybindingItems: IPickerQuickAccessItem[] | undefined } | undefined { + if (!value) { + return; + } + const configureKeybindingItems: IPickerQuickAccessItem[] = []; + const configuredKeybindingItems: IPickerQuickAccessItem[] = []; + const matches = value.matchAll(/(\[^\<]*)\>)/gm); + for (const match of [...matches]) { + const commandId = match?.groups?.commandId; + let kbLabel; + if (match?.length && commandId) { + const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); + if (!keybinding) { + kbLabel = ` (unassigned keybinding)`; + configureKeybindingItems.push({ + label: commandId, + id: commandId + }); + } else { + kbLabel = ' (' + keybinding + ')'; + configuredKeybindingItems.push({ + label: commandId, + id: commandId + }); + } + value = value.replace(match[0], kbLabel); + } + } + const content = new MarkdownString(value); + content.isTrusted = true; + return { content, configureKeybindingItems: configureKeybindingItems.length ? configureKeybindingItems : undefined, configuredKeybindingItems: configuredKeybindingItems.length ? configuredKeybindingItems : undefined }; +} + diff --git a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts index a28f965e588..ce6c600768b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts @@ -7,28 +7,24 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; -import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibleViewProviderId, AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { descriptionForCommand } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; -import { IAccessibleViewService, IAccessibleContentProvider, IAccessibleViewOptions, AccessibleViewType } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { CommentAccessibilityHelpNLS } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; -import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; +import { IAccessibleViewService, IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; export class EditorAccessibilityHelpContribution extends Disposable { static ID: 'editorAccessibilityHelpContribution'; constructor() { super(); - this._register(AccessibilityHelpAction.addImplementation(95, 'editor', async accessor => { + this._register(AccessibilityHelpAction.addImplementation(90, 'editor', async accessor => { const codeEditorService = accessor.get(ICodeEditorService); const accessibleViewService = accessor.get(IAccessibleViewService); const instantiationService = accessor.get(IInstantiationService); @@ -39,11 +35,11 @@ export class EditorAccessibilityHelpContribution extends Disposable { codeEditor = codeEditorService.getActiveCodeEditor()!; } accessibleViewService.show(instantiationService.createInstance(EditorAccessibilityHelpProvider, codeEditor)); - }, EditorContextKeys.focus)); + })); } } -class EditorAccessibilityHelpProvider implements IAccessibleContentProvider { +class EditorAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { id = AccessibleViewProviderId.Editor; onClose() { this._editor.focus(); @@ -55,6 +51,7 @@ class EditorAccessibilityHelpProvider implements IAccessibleContentProvider { @IKeybindingService private readonly _keybindingService: IKeybindingService, @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { + super(); } provideContent(): string { @@ -89,39 +86,33 @@ class EditorAccessibilityHelpProvider implements IAccessibleContentProvider { } if (options.get(EditorOption.stickyScroll).enabled) { - content.push(descriptionForCommand('editor.action.focusStickyScroll', AccessibilityHelpNLS.stickScrollKb, AccessibilityHelpNLS.stickScrollNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.stickScroll); } if (options.get(EditorOption.tabFocusMode)) { - content.push(descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOnMsg, AccessibilityHelpNLS.tabFocusModeOnMsgNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.tabFocusModeOnMsg); } else { - content.push(descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOffMsg, AccessibilityHelpNLS.tabFocusModeOffMsgNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.tabFocusModeOffMsg); } - return content.join('\n\n'); + content.push(AccessibilityHelpNLS.startDebugging); + content.push(AccessibilityHelpNLS.setBreakpoint); + content.push(AccessibilityHelpNLS.debugExecuteSelection); + content.push(AccessibilityHelpNLS.addToWatch); + return content.join('\n'); } } export function getCommentCommandInfo(keybindingService: IKeybindingService, contextKeyService: IContextKeyService, editor: ICodeEditor): string | undefined { const editorContext = contextKeyService.getContext(editor.getDomNode()!); if (editorContext.getValue(CommentContextKeys.activeEditorHasCommentingRange.key)) { - const commentCommandInfo: string[] = []; - commentCommandInfo.push(CommentAccessibilityHelpNLS.intro); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.NextThread, CommentAccessibilityHelpNLS.nextCommentThreadKb, CommentAccessibilityHelpNLS.nextCommentThreadNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.PreviousThread, CommentAccessibilityHelpNLS.previousCommentThreadKb, CommentAccessibilityHelpNLS.previousCommentThreadNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb, keybindingService)); - return commentCommandInfo.join('\n'); + return [CommentAccessibilityHelpNLS.intro, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.nextCommentThread, CommentAccessibilityHelpNLS.previousCommentThread, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.previousRange].join('\n'); } return; } export function getChatCommandInfo(keybindingService: IKeybindingService, contextKeyService: IContextKeyService): string | undefined { - if (CONTEXT_PROVIDER_EXISTS.getValue(contextKeyService)) { - const commentCommandInfo: string[] = []; - commentCommandInfo.push(descriptionForCommand('workbench.action.quickchat.toggle', AccessibilityHelpNLS.quickChat, AccessibilityHelpNLS.quickChatNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand('inlineChat.start', AccessibilityHelpNLS.startInlineChat, AccessibilityHelpNLS.startInlineChatNoKb, keybindingService)); - return commentCommandInfo.join('\n'); + if (CONTEXT_CHAT_ENABLED.getValue(contextKeyService)) { + return [AccessibilityHelpNLS.quickChat, AccessibilityHelpNLS.startInlineChat].join('\n'); } return; } diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts new file mode 100644 index 00000000000..7c18982222f --- /dev/null +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableMap, IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AccessibleViewType, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; +import { IViewsRegistry, Extensions, IViewDescriptor } from 'vs/workbench/common/views'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; + +export class ExtensionAccessibilityHelpDialogContribution extends Disposable { + static ID = 'extensionAccessibilityHelpDialogContribution'; + private _viewHelpDialogMap = this._register(new DisposableMap()); + constructor(@IKeybindingService keybindingService: IKeybindingService) { + super(); + this._register(Registry.as(Extensions.ViewsRegistry).onViewsRegistered(e => { + for (const view of e) { + for (const viewDescriptor of view.views) { + if (viewDescriptor.accessibilityHelpContent) { + this._viewHelpDialogMap.set(viewDescriptor.id, registerAccessibilityHelpAction(keybindingService, viewDescriptor)); + } + } + } + })); + this._register(Registry.as(Extensions.ViewsRegistry).onViewsDeregistered(e => { + for (const viewDescriptor of e.views) { + if (viewDescriptor.accessibilityHelpContent) { + this._viewHelpDialogMap.get(viewDescriptor.id)?.dispose(); + } + } + })); + } +} + +function registerAccessibilityHelpAction(keybindingService: IKeybindingService, viewDescriptor: IViewDescriptor): IDisposable { + const disposableStore = new DisposableStore(); + const content = viewDescriptor.accessibilityHelpContent?.value; + if (!content) { + throw new Error('No content provided for the accessibility help dialog'); + } + disposableStore.add(AccessibleViewRegistry.register({ + priority: 95, + name: viewDescriptor.id, + type: AccessibleViewType.Help, + when: FocusedViewContext.isEqualTo(viewDescriptor.id), + getProvider: (accessor: ServicesAccessor) => { + const viewsService = accessor.get(IViewsService); + return new ExtensionContentProvider( + viewDescriptor.id, + { type: AccessibleViewType.Help }, + () => content, + () => viewsService.openView(viewDescriptor.id, true), + ); + }, + })); + + disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { + disposableStore.clear(); + disposableStore.add(registerAccessibilityHelpAction(keybindingService, viewDescriptor)); + })); + return disposableStore; +} diff --git a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts index 2fb8316bdc8..7e84a29bc75 100644 --- a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts +++ b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts @@ -10,5 +10,10 @@ export const enum AccessibilityCommandId { GoToSymbol = 'editor.action.accessibleViewGoToSymbol', ShowNext = 'editor.action.accessibleViewNext', ShowPrevious = 'editor.action.accessibleViewPrevious', - AccessibleViewAcceptInlineCompletion = 'editor.action.accessibleViewAcceptInlineCompletion' + AccessibleViewAcceptInlineCompletion = 'editor.action.accessibleViewAcceptInlineCompletion', + NextCodeBlock = 'editor.action.accessibleViewNextCodeBlock', + PreviousCodeBlock = 'editor.action.accessibleViewPreviousCodeBlock', + AccessibilityHelpConfigureKeybindings = 'editor.action.accessibilityHelpConfigureKeybindings', + AccessibilityHelpConfigureAssignedKeybindings = 'editor.action.accessibilityHelpConfigureAssignedKeybindings', + AccessibilityHelpOpenHelpLink = 'editor.action.accessibilityHelpOpenHelpLink', } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts index 8469855da9f..deb77085819 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts @@ -3,175 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ShowAccessibilityAnnouncementHelp, ShowSignalSoundHelp } from 'vs/workbench/contrib/accessibilitySignals/browser/commands'; -import { localize } from 'vs/nls'; +import { AccessibilitySignalService, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { registerAction2 } from 'vs/platform/actions/common/actions'; -import { Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IAccessibilitySignalService, AccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { AccessibilitySignalLineDebuggerContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution'; -import { SignalLineFeatureContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution'; +import { ShowAccessibilityAnnouncementHelp, ShowSignalSoundHelp } from 'vs/workbench/contrib/accessibilitySignals/browser/commands'; +import { EditorTextPropertySignalsContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution'; +import { wrapInReloadableClass0 } from 'vs/platform/observable/common/wrapInReloadableClass'; registerSingleton(IAccessibilitySignalService, AccessibilitySignalService, InstantiationType.Delayed); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SignalLineFeatureContribution, LifecyclePhase.Restored); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AccessibilitySignalLineDebuggerContribution, LifecyclePhase.Restored); - -export const soundFeatureBase: IConfigurationPropertySchema = { - 'type': 'string', - 'enum': ['auto', 'on', 'off'], - 'default': 'auto', - 'enumDescriptions': [ - localize('audioCues.enabled.auto', "Enable sound when a screen reader is attached."), - localize('audioCues.enabled.on', "Enable sound."), - localize('audioCues.enabled.off', "Disable sound.") - ], - tags: ['accessibility'], -}; -const markdownDeprecationMessage = localize('audioCues.enabled.deprecated', "This setting is deprecated. Use `signals` settings instead."); -const soundDeprecatedFeatureBase: IConfigurationPropertySchema = { - ...soundFeatureBase, - markdownDeprecationMessage -}; - -Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ - 'properties': { - 'audioCues.enabled': { - markdownDeprecationMessage: 'Deprecated. Use the specific setting for each audio cue instead (`audioCues.*`).', - tags: ['accessibility'] - }, - 'audioCues.volume': { - markdownDeprecationMessage: 'Deprecated. Use `accessibility.signals.sounds.volume` instead.', - tags: ['accessibility'] - }, - 'audioCues.debouncePositionChanges': { - 'description': localize('audioCues.debouncePositionChanges', "Whether or not position changes should be debounced"), - 'type': 'boolean', - 'default': false, - tags: ['accessibility'], - 'markdownDeprecationMessage': localize('audioCues.debouncePositionChangesDeprecated', 'This setting is deprecated, instead use the `signals.debouncePositionChanges` setting.') - }, - 'audioCues.lineHasBreakpoint': { - 'description': localize('audioCues.lineHasBreakpoint', "Plays a sound when the active line has a breakpoint."), - ...soundDeprecatedFeatureBase - }, - 'audioCues.lineHasInlineSuggestion': { - 'description': localize('audioCues.lineHasInlineSuggestion', "Plays a sound when the active line has an inline suggestion."), - ...soundDeprecatedFeatureBase - }, - 'audioCues.lineHasError': { - 'description': localize('audioCues.lineHasError', "Plays a sound when the active line has an error."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.lineHasFoldedArea': { - 'description': localize('audioCues.lineHasFoldedArea', "Plays a sound when the active line has a folded area that can be unfolded."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.lineHasWarning': { - 'description': localize('audioCues.lineHasWarning', "Plays a sound when the active line has a warning."), - ...soundDeprecatedFeatureBase, - default: 'off', - }, - 'audioCues.onDebugBreak': { - 'description': localize('audioCues.onDebugBreak', "Plays a sound when the debugger stopped on a breakpoint."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.noInlayHints': { - 'description': localize('audioCues.noInlayHints', "Plays a sound when trying to read a line with inlay hints that has no inlay hints."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.taskCompleted': { - 'description': localize('audioCues.taskCompleted', "Plays a sound when a task is completed."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.taskFailed': { - 'description': localize('audioCues.taskFailed', "Plays a sound when a task fails (non-zero exit code)."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.terminalCommandFailed': { - 'description': localize('audioCues.terminalCommandFailed', "Plays a sound when a terminal command fails (non-zero exit code)."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.terminalQuickFix': { - 'description': localize('audioCues.terminalQuickFix', "Plays a sound when terminal Quick Fixes are available."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.terminalBell': { - 'description': localize('audioCues.terminalBell', "Plays a sound when the terminal bell is ringing."), - ...soundDeprecatedFeatureBase, - default: 'on' - }, - 'audioCues.diffLineInserted': { - 'description': localize('audioCues.diffLineInserted', "Plays a sound when the focus moves to an inserted line in Accessible Diff Viewer mode or to the next/previous change."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.diffLineDeleted': { - 'description': localize('audioCues.diffLineDeleted', "Plays a sound when the focus moves to a deleted line in Accessible Diff Viewer mode or to the next/previous change."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.diffLineModified': { - 'description': localize('audioCues.diffLineModified', "Plays a sound when the focus moves to a modified line in Accessible Diff Viewer mode or to the next/previous change."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.notebookCellCompleted': { - 'description': localize('audioCues.notebookCellCompleted', "Plays a sound when a notebook cell execution is successfully completed."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.notebookCellFailed': { - 'description': localize('audioCues.notebookCellFailed', "Plays a sound when a notebook cell execution fails."), - ...soundDeprecatedFeatureBase, - }, - 'audioCues.chatRequestSent': { - 'description': localize('audioCues.chatRequestSent', "Plays a sound when a chat request is made."), - ...soundDeprecatedFeatureBase, - default: 'off' - }, - 'audioCues.chatResponsePending': { - 'description': localize('audioCues.chatResponsePending', "Plays a sound on loop while the response is pending."), - ...soundDeprecatedFeatureBase, - default: 'auto' - }, - 'audioCues.chatResponseReceived': { - 'description': localize('audioCues.chatResponseReceived', "Plays a sound on loop while the response has been received."), - ...soundDeprecatedFeatureBase, - default: 'off' - }, - 'audioCues.clear': { - 'description': localize('audioCues.clear', "Plays a sound when a feature is cleared (for example, the terminal, Debug Console, or Output channel). When this is disabled, an ARIA alert will announce 'Cleared'."), - ...soundDeprecatedFeatureBase, - default: 'off' - }, - 'audioCues.save': { - 'markdownDescription': localize('audioCues.save', "Plays a sound when a file is saved. Also see {0}", '`#accessibility.alert.save#`'), - 'type': 'string', - 'enum': ['userGesture', 'always', 'never'], - 'default': 'never', - 'enumDescriptions': [ - localize('audioCues.save.userGesture', "Plays the audio cue when a user explicitly saves a file."), - localize('audioCues.save.always', "Plays the audio cue whenever a file is saved, including auto save."), - localize('audioCues.save.never', "Never plays the audio cue.") - ], - tags: ['accessibility'], - markdownDeprecationMessage - }, - 'audioCues.format': { - 'markdownDescription': localize('audioCues.format', "Plays a sound when a file or notebook is formatted. Also see {0}", '`#accessibility.alert.format#`'), - 'type': 'string', - 'enum': ['userGesture', 'always', 'never'], - 'default': 'never', - 'enumDescriptions': [ - localize('audioCues.format.userGesture', "Plays the audio cue when a user explicitly formats a file."), - localize('audioCues.format.always', "Plays the audio cue whenever a file is formatted, including if it is set to format on save, type, or, paste, or run of a cell."), - localize('audioCues.format.never', "Never plays the audio cue.") - ], - tags: ['accessibility'], - markdownDeprecationMessage - }, - }, -}); +registerWorkbenchContribution2('EditorTextPropertySignalsContribution', wrapInReloadableClass0(() => EditorTextPropertySignalsContribution), WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2('AccessibilitySignalLineDebuggerContribution', AccessibilitySignalLineDebuggerContribution, WorkbenchPhase.AfterRestored); registerAction2(ShowSignalSoundHelp); registerAction2(ShowAccessibilityAnnouncementHelp); diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts index 45d5f2b3121..f0bc218c0c1 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts @@ -19,7 +19,7 @@ export class AccessibilitySignalLineDebuggerContribution ) { super(); - const isEnabled = observableFromEvent( + const isEnabled = observableFromEvent(this, accessibilitySignalService.onSoundEnabledChanged(AccessibilitySignal.onDebugBreak), () => accessibilitySignalService.isSoundEnabled(AccessibilitySignal.onDebugBreak) ); @@ -63,6 +63,5 @@ export class AccessibilitySignalLineDebuggerContribution this.accessibilitySignalService.playSignal(AccessibilitySignal.onDebugBreak); } }); - } } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts deleted file mode 100644 index 44a8f0236d0..00000000000 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalLineFeatureContribution.ts +++ /dev/null @@ -1,253 +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 { CachedFunction } from 'vs/base/common/cache'; -import { Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, autorun, autorunDelta, derived, derivedOpts, observableFromEvent, observableFromPromise, wasEventTriggeredRecently } from 'vs/base/common/observable'; -import { debouncedObservable2, observableSignalFromEvent } from 'vs/base/common/observableInternal/utils'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { Position } from 'vs/editor/common/core/position'; -import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; -import { ITextModel } from 'vs/editor/common/model'; -import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; -import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; - -export class SignalLineFeatureContribution - extends Disposable - implements IWorkbenchContribution { - private readonly store = this._register(new DisposableStore()); - - private readonly features: LineFeature[] = [ - this.instantiationService.createInstance(MarkerLineFeature, AccessibilitySignal.error, MarkerSeverity.Error), - this.instantiationService.createInstance(MarkerLineFeature, AccessibilitySignal.warning, MarkerSeverity.Warning), - this.instantiationService.createInstance(FoldedAreaLineFeature), - this.instantiationService.createInstance(BreakpointLineFeature), - ]; - - private readonly isEnabledCache = new CachedFunction>((cue) => observableFromEvent( - Event.any( - this.accessibilitySignalService.onSoundEnabledChanged(cue), - this.accessibilitySignalService.onAnnouncementEnabledChanged(cue), - ), - () => this.accessibilitySignalService.isSoundEnabled(cue) || this.accessibilitySignalService.isAnnouncementEnabled(cue) - )); - - private readonly _someAccessibilitySignalIsEnabled = derived(this, - (reader) => this.features.some((feature) => - this.isEnabledCache.get(feature.signal).read(reader) - ) - ); - - private readonly _activeEditorObservable = observableFromEvent( - this.editorService.onDidActiveEditorChange, - (_) => { - const activeTextEditorControl = - this.editorService.activeTextEditorControl; - - const editor = isDiffEditor(activeTextEditorControl) - ? activeTextEditorControl.getOriginalEditor() - : isCodeEditor(activeTextEditorControl) - ? activeTextEditorControl - : undefined; - - return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined; - } - ); - - constructor( - @IEditorService private readonly editorService: IEditorService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(); - - - this._register( - autorun(reader => { - /** @description updateSignalsEnabled */ - this.store.clear(); - - if (!this._someAccessibilitySignalIsEnabled.read(reader)) { - return; - } - const activeEditor = this._activeEditorObservable.read(reader); - if (activeEditor) { - this.registerAccessibilitySignalsForEditor(activeEditor.editor, activeEditor.model, this.store); - } - }) - ); - } - - private registerAccessibilitySignalsForEditor( - editor: ICodeEditor, - editorModel: ITextModel, - store: DisposableStore - ): void { - const curPosition = observableFromEvent( - editor.onDidChangeCursorPosition, - (args) => { - /** @description editor.onDidChangeCursorPosition (caused by user) */ - if ( - args && - args.reason !== CursorChangeReason.Explicit && - args.reason !== CursorChangeReason.NotSet - ) { - // Ignore cursor changes caused by navigation (e.g. which happens when execution is paused). - return undefined; - } - return editor.getPosition(); - } - ); - const debouncedPosition = debouncedObservable2(curPosition, this._configurationService.getValue('accessibility.signals.debouncePositionChanges') ? 300 : 0); - const isTyping = wasEventTriggeredRecently( - e => editorModel.onDidChangeContent(e), - 1000, - store - ); - - const featureStates = this.features.map((feature) => { - const lineFeatureState = feature.createSource(editor, editorModel); - const isFeaturePresent = derivedOpts( - { debugName: `isPresentInLine:${feature.signal.name}` }, - (reader) => { - if (!this.isEnabledCache.get(feature.signal).read(reader)) { - return false; - } - const position = debouncedPosition.read(reader); - if (!position) { - return false; - } - return lineFeatureState.isPresent(position, reader); - } - ); - return derivedOpts( - { debugName: `typingDebouncedFeatureState:\n${feature.signal.name}` }, - (reader) => - feature.debounceWhileTyping && isTyping.read(reader) - ? (debouncedPosition.read(reader), isFeaturePresent.get()) - : isFeaturePresent.read(reader) - ); - }); - - const state = derived( - (reader) => /** @description states */({ - lineNumber: debouncedPosition.read(reader), - featureStates: new Map( - this.features.map((feature, idx) => [ - feature, - featureStates[idx].read(reader), - ]) - ), - }) - ); - - store.add( - autorunDelta(state, ({ lastValue, newValue }) => { - /** @description Play Accessibility Signal */ - const newFeatures = this.features.filter( - feature => - newValue?.featureStates.get(feature) && - (!lastValue?.featureStates?.get(feature) || newValue.lineNumber !== lastValue.lineNumber) - ); - - this.accessibilitySignalService.playSignals(newFeatures.map(f => f.signal)); - }) - ); - } -} - -interface LineFeature { - readonly signal: AccessibilitySignal; - readonly debounceWhileTyping?: boolean; - createSource( - editor: ICodeEditor, - model: ITextModel - ): LineFeatureSource; -} - -interface LineFeatureSource { - isPresent(position: Position, reader: IReader): boolean; -} - -class MarkerLineFeature implements LineFeature { - public readonly debounceWhileTyping = true; - private _previousLine: number = 0; - constructor( - public readonly signal: AccessibilitySignal, - private readonly severity: MarkerSeverity, - @IMarkerService private readonly markerService: IMarkerService, - - ) { } - - createSource(editor: ICodeEditor, model: ITextModel): LineFeatureSource { - const obs = observableSignalFromEvent('onMarkerChanged', this.markerService.onMarkerChanged); - return { - isPresent: (position, reader) => { - obs.read(reader); - const lineChanged = position.lineNumber !== this._previousLine; - this._previousLine = position.lineNumber; - const hasMarker = this.markerService - .read({ resource: model.uri }) - .some( - (m) => { - const onLine = m.severity === this.severity && m.startLineNumber <= position.lineNumber && position.lineNumber <= m.endLineNumber; - return lineChanged ? onLine : onLine && (position.lineNumber <= m.endLineNumber && m.startColumn <= position.column && m.endColumn >= position.column); - }); - return hasMarker; - }, - }; - } -} - -class FoldedAreaLineFeature implements LineFeature { - public readonly signal = AccessibilitySignal.foldedArea; - - createSource(editor: ICodeEditor, _model: ITextModel): LineFeatureSource { - const foldingController = FoldingController.get(editor); - if (!foldingController) { - return { isPresent: () => false, }; - } - const foldingModel = observableFromPromise(foldingController.getFoldingModel() ?? Promise.resolve(undefined)); - return { - isPresent: (position, reader) => { - const m = foldingModel.read(reader); - const regionAtLine = m.value?.getRegionAtLine(position.lineNumber); - const hasFolding = !regionAtLine - ? false - : regionAtLine.isCollapsed && - regionAtLine.startLineNumber === position.lineNumber; - return hasFolding; - }, - }; - } -} - -class BreakpointLineFeature implements LineFeature { - public readonly signal = AccessibilitySignal.break; - - constructor(@IDebugService private readonly debugService: IDebugService) { } - - createSource(editor: ICodeEditor, model: ITextModel): LineFeatureSource { - const signal = observableSignalFromEvent('onDidChangeBreakpoints', this.debugService.getModel().onDidChangeBreakpoints); - return { - isPresent: (position, reader) => { - signal.read(reader); - const breakpoints = this.debugService - .getModel() - .getBreakpoints({ uri: model.uri, lineNumber: position.lineNumber }); - const hasBreakpoints = breakpoints.length > 0; - return hasBreakpoints; - }, - }; - } -} diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts index ec79963a27f..a20ca9f9dc9 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts @@ -8,7 +8,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { localize, localize2 } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { Action2 } from 'vs/platform/actions/common/actions'; -import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { AccessibilitySignal, AcknowledgeDocCommentsToken, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -23,7 +23,7 @@ export class ShowSignalSoundHelp extends Action2 { title: localize2('signals.sound.help', "Help: List Signal Sounds"), f1: true, metadata: { - description: localize('accessibility.sound.help.description', "List all accessibility sounds / audio cues and configure their settings") + description: localize('accessibility.sound.help.description', "List all accessibility sounds, noises, or audio cues and configure their settings") } }); } @@ -43,7 +43,7 @@ export class ShowSignalSoundHelp extends Action2 { tooltip: localize('sounds.help.settings', 'Configure Sound'), alwaysVisible: true }] : [] - })); + })).sort((a, b) => a.label.localeCompare(b.label)); const qp = quickInputService.createQuickPick(); qp.items = items; qp.selectedItems = items.filter(i => accessibilitySignalService.isSoundEnabled(i.signal) || userGestureSignals.includes(i.signal) && configurationService.getValue(i.signal.settingsKey + '.sound') !== 'never'); @@ -59,14 +59,12 @@ export class ShowSignalSoundHelp extends Action2 { configurationService.updateValue(signal.settingsKey, { sound }); } } + for (const signal of disabledSounds) { - let { sound, announcement } = configurationService.getValue<{ sound: string; announcement?: string }>(signal.settingsKey); - sound = userGestureSignals.includes(signal) ? 'never' : 'off'; - if (announcement) { - configurationService.updateValue(signal.settingsKey, { sound, announcement }); - } else { - configurationService.updateValue(signal.settingsKey, { sound }); - } + const announcement = configurationService.getValue(signal.settingsKey + '.announcement'); + const sound = getDisabledSettingValue(userGestureSignals.includes(signal), accessibilityService.isScreenReaderOptimized()); + const value = announcement ? { sound, announcement } : { sound }; + configurationService.updateValue(signal.settingsKey, value); } qp.hide(); }); @@ -74,7 +72,7 @@ export class ShowSignalSoundHelp extends Action2 { preferencesService.openUserSettings({ jsonEditor: true, revealSetting: { key: e.item.signal.settingsKey, edit: true } }); }); qp.onDidChangeActive(() => { - accessibilitySignalService.playSound(qp.activeItems[0].signal.sound.getSound(true), true); + accessibilitySignalService.playSound(qp.activeItems[0].signal.sound.getSound(true), true, AcknowledgeDocCommentsToken); }); qp.placeholder = localize('sounds.help.placeholder', 'Select a sound to play and configure'); qp.canSelectMany = true; @@ -82,6 +80,10 @@ export class ShowSignalSoundHelp extends Action2 { } } +function getDisabledSettingValue(isUserGestureSignal: boolean, isScreenReaderOptimized: boolean): string { + return isScreenReaderOptimized ? (isUserGestureSignal ? 'never' : 'off') : (isUserGestureSignal ? 'never' : 'auto'); +} + export class ShowAccessibilityAnnouncementHelp extends Action2 { static readonly ID = 'accessibility.announcement.help'; @@ -91,7 +93,7 @@ export class ShowAccessibilityAnnouncementHelp extends Action2 { title: localize2('accessibility.announcement.help', "Help: List Signal Announcements"), f1: true, metadata: { - description: localize('accessibility.announcement.help.description', "List all accessibility announcements / alerts and configure their settings") + description: localize('accessibility.announcement.help.description', "List all accessibility announcements, alerts, braille messages, and configure their settings") } }); } @@ -111,11 +113,17 @@ export class ShowAccessibilityAnnouncementHelp extends Action2 { tooltip: localize('announcement.help.settings', 'Configure Announcement'), alwaysVisible: true, }] : [] - })); + })).sort((a, b) => a.label.localeCompare(b.label)); const qp = quickInputService.createQuickPick(); qp.items = items; qp.selectedItems = items.filter(i => accessibilitySignalService.isAnnouncementEnabled(i.signal) || userGestureSignals.includes(i.signal) && configurationService.getValue(i.signal.settingsKey + '.announcement') !== 'never'); + const screenReaderOptimized = accessibilityService.isScreenReaderOptimized(); qp.onDidAccept(() => { + if (!screenReaderOptimized) { + // announcements are off by default when screen reader is not active + qp.hide(); + return; + } const enabledAnnouncements = qp.selectedItems.map(i => i.signal); const disabledAnnouncements = AccessibilitySignal.allAccessibilitySignals.filter(cue => !!cue.legacyAnnouncementSettingsKey && !enabledAnnouncements.includes(cue)); for (const signal of enabledAnnouncements) { @@ -123,17 +131,19 @@ export class ShowAccessibilityAnnouncementHelp extends Action2 { announcement = userGestureSignals.includes(signal) ? 'userGesture' : signal.announcementMessage && accessibilityService.isScreenReaderOptimized() ? 'auto' : undefined; configurationService.updateValue(signal.settingsKey, { sound, announcement }); } + for (const signal of disabledAnnouncements) { - const announcement = userGestureSignals.includes(signal) ? 'never' : 'off'; + const announcement = getDisabledSettingValue(userGestureSignals.includes(signal), true); const sound = configurationService.getValue(signal.settingsKey + '.sound'); - configurationService.updateValue(signal.settingsKey, announcement ? { sound, announcement } : { sound }); + const value = announcement ? { sound, announcement } : { sound }; + configurationService.updateValue(signal.settingsKey, value); } qp.hide(); }); qp.onDidTriggerItemButton(e => { preferencesService.openUserSettings({ jsonEditor: true, revealSetting: { key: e.item.signal.settingsKey, edit: true } }); }); - qp.placeholder = localize('announcement.help.placeholder', 'Select an announcement to configure'); + qp.placeholder = screenReaderOptimized ? localize('announcement.help.placeholder', 'Select an announcement to configure') : localize('announcement.help.placeholder.disabled', 'Screen reader is not active, announcements are disabled by default.'); qp.canSelectMany = true; await qp.show(); } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts new file mode 100644 index 00000000000..f5dddf8ada4 --- /dev/null +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -0,0 +1,277 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { disposableTimeout } from 'vs/base/common/async'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IReader, autorun, autorunWithStore, derived, observableFromEvent, observableFromPromise } from 'vs/base/common/observable'; +import { observableFromValueWithChangeEvent, observableSignalFromEvent, wasEventTriggeredRecently } from 'vs/base/common/observableInternal/utils'; +import { isDefined } from 'vs/base/common/types'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; +import { ITextModel } from 'vs/editor/common/model'; +import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; +import { AccessibilitySignal, AccessibilityModality, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class EditorTextPropertySignalsContribution extends Disposable implements IWorkbenchContribution { + private readonly _textProperties: TextProperty[] = [ + this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.errorAtPosition, AccessibilitySignal.errorOnLine, MarkerSeverity.Error), + this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.warningAtPosition, AccessibilitySignal.warningOnLine, MarkerSeverity.Warning), + this._instantiationService.createInstance(FoldedAreaTextProperty), + this._instantiationService.createInstance(BreakpointTextProperty), + ]; + + private readonly _someAccessibilitySignalIsEnabled = derived(this, reader => + this._textProperties + .flatMap(p => [p.lineSignal, p.positionSignal]) + .filter(isDefined) + .some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader)) + ); + + private readonly _activeEditorObservable = observableFromEvent(this, + this._editorService.onDidActiveEditorChange, + (_) => { + const activeTextEditorControl = this._editorService.activeTextEditorControl; + + const editor = isDiffEditor(activeTextEditorControl) + ? activeTextEditorControl.getOriginalEditor() + : isCodeEditor(activeTextEditorControl) + ? activeTextEditorControl + : undefined; + + return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined; + } + ); + + constructor( + @IEditorService private readonly _editorService: IEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService + ) { + super(); + + this._register(autorunWithStore((reader, store) => { + /** @description updateSignalsEnabled */ + if (!this._someAccessibilitySignalIsEnabled.read(reader)) { + return; + } + const activeEditor = this._activeEditorObservable.read(reader); + if (activeEditor) { + this._registerAccessibilitySignalsForEditor(activeEditor.editor, activeEditor.model, store); + } + })); + } + + private _registerAccessibilitySignalsForEditor(editor: ICodeEditor, editorModel: ITextModel, store: DisposableStore): void { + let lastLine = -1; + const ignoredLineSignalsForCurrentLine = new Set(); + + const timeouts = store.add(new DisposableStore()); + + const propertySources = this._textProperties.map(p => ({ source: p.createSource(editor, editorModel), property: p })); + + const didType = wasEventTriggeredRecently(editor.onDidChangeModelContent, 100, store); + + store.add(editor.onDidChangeCursorPosition(args => { + timeouts.clear(); + + if ( + args && + args.reason !== CursorChangeReason.Explicit && + args.reason !== CursorChangeReason.NotSet + ) { + // Ignore cursor changes caused by navigation (e.g. which happens when execution is paused). + ignoredLineSignalsForCurrentLine.clear(); + return; + } + + const trigger = (property: TextProperty, source: TextPropertySource, mode: 'line' | 'positional') => { + const signal = mode === 'line' ? property.lineSignal : property.positionSignal; + if ( + !signal + || !this._accessibilitySignalService.getEnabledState(signal, false).value + || !source.isPresent(position, mode, undefined) + ) { + return; + } + + for (const modality of ['sound', 'announcement'] as AccessibilityModality[]) { + if (this._accessibilitySignalService.getEnabledState(signal, false, modality).value) { + const delay = this._accessibilitySignalService.getDelayMs(signal, modality, mode) + (didType.get() ? 1000 : 0); + + timeouts.add(disposableTimeout(() => { + if (source.isPresent(position, mode, undefined)) { + if (!(mode === 'line') || !ignoredLineSignalsForCurrentLine.has(property)) { + this._accessibilitySignalService.playSignal(signal, { modality }); + } + ignoredLineSignalsForCurrentLine.add(property); + } + }, delay)); + } + } + }; + + // React to cursor changes + const position = args.position; + const lineNumber = position.lineNumber; + if (lineNumber !== lastLine) { + ignoredLineSignalsForCurrentLine.clear(); + lastLine = lineNumber; + for (const p of propertySources) { + trigger(p.property, p.source, 'line'); + } + } + for (const p of propertySources) { + trigger(p.property, p.source, 'positional'); + } + + // React to property state changes for the current cursor position + for (const s of propertySources) { + if ( + ![s.property.lineSignal, s.property.positionSignal] + .some(s => s && this._accessibilitySignalService.getEnabledState(s, false).value) + ) { + return; + } + + let lastValueAtPosition: boolean | undefined = undefined; + let lastValueOnLine: boolean | undefined = undefined; + timeouts.add(autorun(reader => { + const newValueAtPosition = s.source.isPresentAtPosition(args.position, reader); + const newValueOnLine = s.source.isPresentOnLine(args.position.lineNumber, reader); + + if (lastValueAtPosition !== undefined && lastValueAtPosition !== undefined) { + if (!lastValueAtPosition && newValueAtPosition) { + trigger(s.property, s.source, 'positional'); + } + if (!lastValueOnLine && newValueOnLine) { + trigger(s.property, s.source, 'line'); + } + } + + lastValueAtPosition = newValueAtPosition; + lastValueOnLine = newValueOnLine; + })); + } + })); + } +} + +interface TextProperty { + readonly positionSignal?: AccessibilitySignal; + readonly lineSignal?: AccessibilitySignal; + readonly debounceWhileTyping?: boolean; + createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource; +} + +class TextPropertySource { + public static notPresent = new TextPropertySource({ isPresentAtPosition: () => false, isPresentOnLine: () => false }); + + public readonly isPresentOnLine: (lineNumber: number, reader: IReader | undefined) => boolean; + public readonly isPresentAtPosition: (position: Position, reader: IReader | undefined) => boolean; + + constructor(options: { + isPresentOnLine: (lineNumber: number, reader: IReader | undefined) => boolean; + isPresentAtPosition?: (position: Position, reader: IReader | undefined) => boolean; + }) { + this.isPresentOnLine = options.isPresentOnLine; + this.isPresentAtPosition = options.isPresentAtPosition ?? (() => false); + } + + public isPresent(position: Position, mode: 'line' | 'positional', reader: IReader | undefined): boolean { + return mode === 'line' ? this.isPresentOnLine(position.lineNumber, reader) : this.isPresentAtPosition(position, reader); + } +} + +class MarkerTextProperty implements TextProperty { + public readonly debounceWhileTyping = true; + constructor( + public readonly positionSignal: AccessibilitySignal, + public readonly lineSignal: AccessibilitySignal, + private readonly severity: MarkerSeverity, + @IMarkerService private readonly markerService: IMarkerService, + + ) { } + + createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource { + const obs = observableSignalFromEvent('onMarkerChanged', this.markerService.onMarkerChanged); + return new TextPropertySource({ + isPresentAtPosition: (position, reader) => { + obs.read(reader); + const hasMarker = this.markerService + .read({ resource: model.uri }) + .some( + (m) => + m.severity === this.severity && + m.startLineNumber <= position.lineNumber && + position.lineNumber <= m.endLineNumber && + m.startColumn <= position.column && + position.column <= m.endColumn + ); + return hasMarker; + }, + isPresentOnLine: (lineNumber, reader) => { + obs.read(reader); + const hasMarker = this.markerService + .read({ resource: model.uri }) + .some( + (m) => + m.severity === this.severity && + m.startLineNumber <= lineNumber && + lineNumber <= m.endLineNumber + ); + return hasMarker; + } + }); + } +} + +class FoldedAreaTextProperty implements TextProperty { + public readonly lineSignal = AccessibilitySignal.foldedArea; + + createSource(editor: ICodeEditor, _model: ITextModel): TextPropertySource { + const foldingController = FoldingController.get(editor); + if (!foldingController) { return TextPropertySource.notPresent; } + + const foldingModel = observableFromPromise(foldingController.getFoldingModel() ?? Promise.resolve(undefined)); + return new TextPropertySource({ + isPresentOnLine(lineNumber, reader): boolean { + const m = foldingModel.read(reader); + const regionAtLine = m.value?.getRegionAtLine(lineNumber); + const hasFolding = !regionAtLine + ? false + : regionAtLine.isCollapsed && + regionAtLine.startLineNumber === lineNumber; + return hasFolding; + } + }); + } +} + +class BreakpointTextProperty implements TextProperty { + public readonly lineSignal = AccessibilitySignal.break; + + constructor(@IDebugService private readonly debugService: IDebugService) { } + + createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource { + const signal = observableSignalFromEvent('onDidChangeBreakpoints', this.debugService.getModel().onDidChangeBreakpoints); + const debugService = this.debugService; + return new TextPropertySource({ + isPresentOnLine(lineNumber, reader): boolean { + signal.read(reader); + const breakpoints = debugService + .getModel() + .getBreakpoints({ uri: model.uri, lineNumber }); + const hasBreakpoints = breakpoints.length > 0; + return hasBreakpoints; + } + }); + } +} diff --git a/src/vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement.ts similarity index 100% rename from src/vs/workbench/contrib/accessibility/browser/openDiffEditorAnnouncement.ts rename to src/vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement.ts diff --git a/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/saveAccessibilitySignal.ts similarity index 100% rename from src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts rename to src/vs/workbench/contrib/accessibilitySignals/browser/saveAccessibilitySignal.ts diff --git a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index 4975185248d..f7c3dceac4d 100644 --- a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts @@ -26,13 +26,11 @@ import { IRequestService, asText } from 'vs/platform/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { isWeb } from 'vs/base/common/platform'; -import { isInternalTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; const accountsBadgeConfigKey = 'workbench.accounts.experimental.showEntitlements'; -const chatWelcomeViewConfigKey = 'workbench.chat.experimental.showWelcomeView'; type EntitlementEnablementClassification = { - enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if the entitlement is enabled' }; + enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the entitlement is enabled' }; owner: 'bhavyaus'; comment: 'Reporting when the entitlement is shown'; }; @@ -47,21 +45,19 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut private isInitialized = false; private showAccountsBadgeContextKey = new RawContextKey(accountsBadgeConfigKey, false).bindTo(this.contextService); - private showChatWelcomeViewContextKey = new RawContextKey(chatWelcomeViewConfigKey, false).bindTo(this.contextService); - private accountsMenuBadgeDisposable = this._register(new MutableDisposable()); + private readonly accountsMenuBadgeDisposable = this._register(new MutableDisposable()); constructor( - @IContextKeyService readonly contextService: IContextKeyService, - @ICommandService readonly commandService: ICommandService, - @ITelemetryService readonly telemetryService: ITelemetryService, - @IAuthenticationService readonly authenticationService: IAuthenticationService, - @IProductService readonly productService: IProductService, - @IStorageService readonly storageService: IStorageService, - @IExtensionManagementService readonly extensionManagementService: IExtensionManagementService, - @IActivityService readonly activityService: IActivityService, - @IExtensionService readonly extensionService: IExtensionService, - @IConfigurationService readonly configurationService: IConfigurationService, - @IRequestService readonly requestService: IRequestService) { + @IContextKeyService private readonly contextService: IContextKeyService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IProductService private readonly productService: IProductService, + @IStorageService private readonly storageService: IStorageService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IActivityService private readonly activityService: IActivityService, + @IExtensionService private readonly extensionService: IExtensionService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IRequestService private readonly requestService: IRequestService) { super(); if (!this.productService.gitHubEntitlement || isWeb) { @@ -80,6 +76,11 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut private registerListeners() { + if (this.storageService.getBoolean(accountsBadgeConfigKey, StorageScope.APPLICATION) === false) { + // we have already shown the entitlements. Do not show again + return; + } + this._register(this.extensionService.onDidChangeExtensions(async (result) => { for (const ext of result.added) { if (ExtensionIdentifier.equals(this.productService.gitHubEntitlement!.extensionId, ext.identifier)) { @@ -94,7 +95,6 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut await this.enableEntitlements(e.event.added[0]); } else if (e.providerId === this.productService.gitHubEntitlement!.providerId && e.event.removed?.length) { this.showAccountsBadgeContextKey.set(false); - this.showChatWelcomeViewContextKey.set(false); this.accountsMenuBadgeDisposable.clear(); } })); @@ -143,34 +143,28 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut } this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>('entitlements.enabled', { enabled: true }); this.isInitialized = true; - const orgs = parsedResult['organization_login_list'] as any[]; - return [true, orgs ? orgs[orgs.length - 1] : undefined]; + const orgs: { login: string; name: string }[] = parsedResult['organization_list'] as { login: string; name: string }[]; + return [true, orgs && orgs.length > 0 ? (orgs[0].name ? orgs[0].name : orgs[0].login) : undefined]; } - private async enableEntitlements(session: AuthenticationSession) { - const isInternal = isInternalTelemetry(this.productService, this.configurationService); + private async enableEntitlements(session: AuthenticationSession | undefined) { + if (!session) { + return; + } + const showAccountsBadge = this.configurationService.inspect(accountsBadgeConfigKey).value ?? false; - const showWelcomeView = this.configurationService.inspect(chatWelcomeViewConfigKey).value ?? false; const [enabled, org] = await this.getEntitlementsInfo(session); - if (enabled) { - if (isInternal && showWelcomeView) { - this.showChatWelcomeViewContextKey.set(true); - this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(chatWelcomeViewConfigKey, { enabled: true }); - } - if (showAccountsBadge) { - this.createAccountsBadge(org); - this.showAccountsBadgeContextKey.set(showAccountsBadge); - this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(accountsBadgeConfigKey, { enabled: true }); - } + if (enabled && showAccountsBadge) { + this.createAccountsBadge(org); + this.showAccountsBadgeContextKey.set(showAccountsBadge); + this.telemetryService.publicLog2<{ enabled: boolean }, EntitlementEnablementClassification>(accountsBadgeConfigKey, { enabled: true }); } } private disableEntitlements() { this.storageService.store(accountsBadgeConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); - this.storageService.store(chatWelcomeViewConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); this.showAccountsBadgeContextKey.set(false); - this.showChatWelcomeViewContextKey.set(false); this.accountsMenuBadgeDisposable.clear(); } @@ -179,7 +173,15 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut const menuTitle = org ? this.productService.gitHubEntitlement!.command.title.replace('{{org}}', org) : this.productService.gitHubEntitlement!.command.titleWithoutPlaceHolder; const badge = new NumberBadge(1, () => menuTitle); - this.accountsMenuBadgeDisposable.value = this.activityService.showAccountsActivity({ badge, }); + this.accountsMenuBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); + + this.contextService.onDidChangeContext(e => { + if (e.affectsSome(new Set([accountsBadgeConfigKey]))) { + if (!this.contextService.getContextKeyValue(accountsBadgeConfigKey)) { + this.accountsMenuBadgeDisposable.clear(); + } + } + }); this._register(registerAction2(class extends Action2 { constructor() { @@ -222,7 +224,7 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut }); } - const contextKey = new RawContextKey(accountsBadgeConfigKey, true).bindTo(contextKeyService); + const contextKey = new RawContextKey(accountsBadgeConfigKey, false).bindTo(contextKeyService); contextKey.set(false); storageService.store(accountsBadgeConfigKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); } @@ -244,17 +246,4 @@ configurationRegistry.registerConfiguration({ } }); -configurationRegistry.registerConfiguration({ - ...applicationConfigurationNodeBase, - properties: { - 'workbench.chat.experimental.showWelcomeView': { - scope: ConfigurationScope.MACHINE, - type: 'boolean', - default: false, - tags: ['experimental'], - description: localize('workbench.chat.showWelcomeView', "When enabled, the chat panel welcome view will be shown.") - } - } -}); - registerWorkbenchContribution2('workbench.contrib.entitlements', EntitlementsContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts index 3c3d4b9aee1..733c0e50037 100644 --- a/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts @@ -113,7 +113,7 @@ export class ManageTrustedExtensionsForAccountAction extends Action2 { } const disposableStore = new DisposableStore(); - const quickPick = disposableStore.add(quickInputService.createQuickPick()); + const quickPick = disposableStore.add(quickInputService.createQuickPick({ useSeparators: true })); quickPick.canSelectMany = true; quickPick.customButton = true; quickPick.customLabel = localize('manageTrustedExtensions.cancel', 'Cancel'); diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts index 36d4e4e1352..4e0a9ab1e3d 100644 --- a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -123,6 +123,10 @@ export class AuthenticationContribution extends Disposable implements IWorkbench this._register(codeExchangeProxyCommand); this._register(extensionFeature); + // Clear the placeholder menu item if there are already providers registered. + if (_authenticationService.getProviderIds().length) { + this._clearPlaceholderMenuItem(); + } this._registerHandlers(); this._registerAuthenticationExtentionPointHandler(); this._registerEnvContributedAuthenticationProviders(); @@ -172,8 +176,7 @@ export class AuthenticationContribution extends Disposable implements IWorkbench private _registerHandlers(): void { this._register(this._authenticationService.onDidRegisterAuthenticationProvider(_e => { - this._placeholderMenuItem?.dispose(); - this._placeholderMenuItem = undefined; + this._clearPlaceholderMenuItem(); })); this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(_e => { if (!this._authenticationService.getProviderIds().length) { @@ -192,6 +195,11 @@ export class AuthenticationContribution extends Disposable implements IWorkbench this._register(registerAction2(SignOutOfAccountAction)); this._register(registerAction2(ManageTrustedExtensionsForAccountAction)); } + + private _clearPlaceholderMenuItem(): void { + this._placeholderMenuItem?.dispose(); + this._placeholderMenuItem = undefined; + } } registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index f0948975790..f11d5cb58cd 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -363,7 +363,7 @@ export class BulkFileEdits { for (let i = 1; i < edits.length; i++) { const edit = edits[i]; const lastGroup = tail(groups); - if (lastGroup[0].type === edit.type) { + if (lastGroup?.[0].type === edit.type) { lastGroup.push(edit); } else { groups.push([edit]); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 8c89ac691ee..a24720d5b75 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -3,43 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./bulkEdit'; -import { WorkbenchAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter, compareBulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { localize } from 'vs/nls'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { BulkEditPreviewProvider, BulkFileOperation, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { URI } from 'vs/base/common/uri'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import type { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { ButtonBar } from 'vs/base/browser/ui/button/button'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import type { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { CachedFunction, LRUCachedFunction } from 'vs/base/common/cache'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mutable } from 'vs/base/common/types'; -import { IResourceDiffEditorInput } from 'vs/workbench/common/editor'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./bulkEdit'; +import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { IMultiDiffEditorOptions, IMultiDiffResourceId } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; import { IRange } from 'vs/editor/common/core/range'; -import { CachedFunction, LRUCachedFunction } from 'vs/base/common/cache'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IOpenEvent, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IMultiDiffEditorResource, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { BulkEditPreviewProvider, BulkFileOperation, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; +import { BulkEditAccessibilityProvider, BulkEditDataSource, BulkEditDelegate, BulkEditElement, BulkEditIdentityProvider, BulkEditNaviLabelProvider, BulkEditSorter, CategoryElement, CategoryElementRenderer, compareBulkFileOperations, FileElement, FileElementRenderer, TextEditElement, TextEditElementRenderer } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const enum State { Data = 'data', @@ -55,7 +56,7 @@ export class BulkEditPane extends ViewPane { static readonly ctxGroupByFile = new RawContextKey('refactorPreview.groupByFile', true); static readonly ctxHasCheckedChanges = new RawContextKey('refactorPreview.hasCheckedChanges', true); - private static readonly _memGroupByFile = `${BulkEditPane.ID}.groupByFile`; + private static readonly _memGroupByFile = `${this.ID}.groupByFile`; private _tree!: WorkbenchAsyncDataTree; private _treeDataSource!: BulkEditDataSource; @@ -89,10 +90,11 @@ export class BulkEditPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IHoverService hoverService: IHoverService, ) { super( { ...options, titleMenuId: MenuId.BulkEditTitle }, - keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instaService, openerService, themeService, telemetryService + keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instaService, openerService, themeService, telemetryService, hoverService ); this.element.classList.add('bulk-edit-panel', 'show-file-icons'); @@ -118,7 +120,7 @@ export class BulkEditPane extends ViewPane { const resourceLabels = this._instaService.createInstance( ResourceLabels, - { onDidChangeVisibility: this.onDidChangeBodyVisibility } + { onDidChangeVisibility: this.onDidChangeBodyVisibility } ); this._disposables.add(resourceLabels); @@ -367,16 +369,20 @@ export class BulkEditPane extends ViewPane { }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } - private readonly _computeResourceDiffEditorInputs = new LRUCachedFunction(async (fileOperations: BulkFileOperation[]) => { - const computeDiffEditorInput = new CachedFunction>(async (fileOperation) => { + private readonly _computeResourceDiffEditorInputs = new LRUCachedFunction< + BulkFileOperation[], + Promise<{ resources: IMultiDiffEditorResource[]; getResourceDiffEditorInputIdOfOperation: (operation: BulkFileOperation) => Promise }> + >(async (fileOperations) => { + const computeDiffEditorInput = new CachedFunction>(async (fileOperation) => { const fileOperationUri = fileOperation.uri; const previewUri = this._currentProvider!.asPreviewUri(fileOperationUri); // delete if (fileOperation.type & BulkFileOperationType.Delete) { return { original: { resource: URI.revive(previewUri) }, - modified: { resource: undefined } - }; + modified: { resource: undefined }, + goToFileResource: fileOperation.uri, + } satisfies IMultiDiffEditorResource; } // rename, create, edits @@ -390,8 +396,9 @@ export class BulkEditPane extends ViewPane { } return { original: { resource: URI.revive(leftResource) }, - modified: { resource: URI.revive(previewUri) } - }; + modified: { resource: URI.revive(previewUri) }, + goToFileResource: leftResource, + } satisfies IMultiDiffEditorResource; } }); @@ -408,7 +415,7 @@ export class BulkEditPane extends ViewPane { resources, getResourceDiffEditorInputIdOfOperation }; - }, key => key); + }); private _onContextMenu(e: ITreeContextMenuEvent): void { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index 40fbb606f8b..44c687a6dee 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -353,7 +353,7 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider { private static readonly Schema = 'vscode-bulkeditpreview-editor'; - static emptyPreview = URI.from({ scheme: BulkEditPreviewProvider.Schema, fragment: 'empty' }); + static emptyPreview = URI.from({ scheme: this.Schema, fragment: 'empty' }); static fromPreviewUri(uri: URI): URI { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index e45a95008c3..45a97a0c67a 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -25,13 +25,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; // --- VIEW MODEL @@ -202,9 +200,7 @@ export class BulkEditDataSource implements IAsyncDataSource')); content.push(localize('chat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.')); content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.')); - content.push(descriptionForCommand('chat.action.focus', localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command ({0}).',), localize('workbench.action.chat.focusNoKb', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat List command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.focusInput', localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command ({0}).'), localize('workbench.action.interactiveSession.focusInputNoKb', 'To focus the input box for chat requests, invoke the Focus Chat Input command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.nextCodeBlock', localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command ({0}).'), localize('workbench.action.chat.nextCodeBlockNoKb', 'To focus the next code block within a response, invoke the Chat: Next Code Block command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.nextFileTree', localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command ({0}).'), localize('workbench.action.chat.nextFileTreeNoKb', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.clear', localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command ({0}).'), localize('workbench.action.chat.clearNoKb', 'To clear the request/response list, invoke the Chat Clear command, which is currently not triggerable by a keybinding.'), keybindingService)); + content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command{0}.', '')); + content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', '')); + content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command{0}.', '')); + content.push(localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command{0}.', '')); + content.push(localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command{0}.', '')); } else { - const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.")); - content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat ({0}).", startChatKeybinding)); - const upHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.previousFromHistory')?.getAriaLabel(); - const downHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.nextFromHistory')?.getAriaLabel(); - if (upHistoryKeybinding && downHistoryKeybinding) { - content.push(localize('inlineChat.requestHistory', 'In the input box, use {0} and {1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', upHistoryKeybinding, downHistoryKeybinding)); - } - content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view {0}.', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat{0}.", '')); + content.push(localize('inlineChat.requestHistory', 'In the input box, use Show Previous{0} and Show Next{1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', '', '')); + content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view{0}.', '')); content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.")); content.push(localize('inlineChat.fix', "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.")); - const diffReviewKeybinding = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - content.push(diffReviewKeybinding ? localize('inlineChat.diff', "Once in the diff editor, enter review mode with ({0}). Use up and down arrows to navigate lines with the proposed changes.", diffReviewKeybinding) : localize('inlineChat.diffNoKb', "Tab again to enter the Diff editor with the changes and enter review mode with the Go to Next Difference Command. Use Up/DownArrow to navigate lines with the proposed changes.")); + content.push(localize('inlineChat.diff', "Once in the diff editor, enter review mode with{0}. Use up and down arrows to navigate lines with the proposed changes.", AccessibleDiffViewerNext.id)); content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); } content.push(localize('chat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring.")); - return content.join('\n\n'); + return content.join('\n'); } -function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return format(msg, kb.getAriaLabel()); - } - return format(noKbMsg, commandId); -} - -export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat'): Promise { +export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat') { const widgetService = accessor.get(IChatWidgetService); - const accessibleViewService = accessor.get(IAccessibleViewService); const inputEditor: ICodeEditor | undefined = type === 'panelChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; if (!inputEditor) { @@ -72,12 +69,12 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi const cachedPosition = inputEditor.getPosition(); inputEditor.getSupportedActions(); - const helpText = getAccessibilityHelpText(accessor, type); - accessibleViewService.show({ - id: type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, - verbositySettingKey: type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, - provideContent: () => helpText, - onClose: () => { + const helpText = getAccessibilityHelpText(type); + return new AccessibleContentProvider( + type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, + { type: AccessibleViewType.Help }, + () => helpText, + () => { if (type === 'panelChat' && cachedPosition) { inputEditor.setPosition(cachedPosition); inputEditor.focus(); @@ -89,6 +86,6 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi } }, - options: { type: AccessibleViewType.Help } - }); + type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, + ); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 81f5f65e1bc..0619ea85f01 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -5,36 +5,37 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { localize, localize2 } from 'vs/nls'; -import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IsLinuxContext, IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IQuickInputButton, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; +import { CHAT_VIEW_ID, IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS, CONTEXT_REQUEST, CONTEXT_RESPONSE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { chatAgentLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +export interface IChatViewTitleActionContext { + chatView: ChatViewPane; +} + +export function isChatViewTitleActionContext(obj: unknown): obj is IChatViewTitleActionContext { + return obj instanceof Object && 'chatView' in obj; +} + export const CHAT_CATEGORY = localize2('chat.category', 'Chat'); export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open'; @@ -47,6 +48,15 @@ export interface IChatViewOpenOptions { * Whether the query is partial and will await more input from the user. */ isPartialQuery?: boolean; + /** + * Any previous chat requests and responses that should be shown in the chat view. + */ + previousRequests?: IChatViewOpenRequestEntry[]; +} + +export interface IChatViewOpenRequestEntry { + request: string; + response: string; } class OpenChatGlobalAction extends Action2 { @@ -54,7 +64,6 @@ class OpenChatGlobalAction extends Action2 { super({ id: CHAT_OPEN_ACTION_ID, title: localize2('openChat', "Open Chat"), - precondition: CONTEXT_PROVIDER_EXISTS, icon: Codicon.commentDiscussion, f1: false, category: CHAT_CATEGORY, @@ -72,15 +81,15 @@ class OpenChatGlobalAction extends Action2 { opts = typeof opts === 'string' ? { query: opts } : opts; const chatService = accessor.get(IChatService); - const chatWidgetService = accessor.get(IChatWidgetService); - const providers = chatService.getProviderInfos(); - if (!providers.length) { - return; - } - const chatWidget = await chatWidgetService.revealViewForProvider(providers[0].id); + const chatWidget = await showChatView(accessor.get(IViewsService)); if (!chatWidget) { return; } + if (opts?.previousRequests?.length && chatWidget.viewModel) { + for (const { request, response } of opts.previousRequests) { + chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response }); + } + } if (opts?.query) { if (opts.isPartialQuery) { chatWidget.setInput(opts.query); @@ -93,93 +102,113 @@ class OpenChatGlobalAction extends Action2 { } } -export class ChatSubmitSecondaryAgentEditorAction extends EditorAction2 { - static readonly ID = 'workbench.action.chat.submitSecondaryAgent'; - +class ChatHistoryAction extends Action2 { constructor() { super({ - id: ChatSubmitSecondaryAgentEditorAction.ID, - title: localize2({ key: 'actions.chat.submitSecondaryAgent', comment: ['Send input from the chat input box to the secondary agent'] }, "Submit to Secondary Agent"), - precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_INPUT_HAS_AGENT.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), - keybinding: { - when: CONTEXT_IN_CHAT_INPUT, - primary: KeyMod.CtrlCmd | KeyCode.Enter, - weight: KeybindingWeight.EditorContrib - }, + id: `workbench.action.chat.history`, + title: localize2('chat.history.label', "Show Chats..."), menu: { - id: MenuId.ChatExecuteSecondary, - group: 'group_1', - when: CONTEXT_CHAT_INPUT_HAS_AGENT.negate(), - } + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), + group: 'navigation', + order: -1 + }, + category: CHAT_CATEGORY, + icon: Codicon.history, + f1: true, + precondition: CONTEXT_CHAT_ENABLED }); } - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { - const editorUri = editor.getModel()?.uri; - if (editorUri) { - const agentService = accessor.get(IChatAgentService); - const secondaryAgent = agentService.getSecondaryAgent(); - if (!secondaryAgent) { - return; - } + async run(accessor: ServicesAccessor) { + const chatService = accessor.get(IChatService); + const quickInputService = accessor.get(IQuickInputService); + const viewsService = accessor.get(IViewsService); + const editorService = accessor.get(IEditorService); - const widgetService = accessor.get(IChatWidgetService); - const widget = widgetService.getWidgetByInputUri(editorUri); - if (!widget) { - return; - } + const openInEditorButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.file), + tooltip: localize('interactiveSession.history.editor', "Open in Editor"), + }; + const deleteButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.x), + tooltip: localize('interactiveSession.history.delete', "Delete"), + }; - if (widget.getInput().match(/^\s*@/)) { - widget.acceptInput(); - } else { - widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.name}`); - } + interface IChatPickerItem extends IQuickPickItem { + chat: IChatDetail; } + + const getPicks = () => { + const items = chatService.getHistory(); + return items.map((i): IChatPickerItem => ({ + label: i.title, + chat: i, + buttons: [ + openInEditorButton, + deleteButton + ] + })); + }; + + const store = new DisposableStore(); + const picker = store.add(quickInputService.createQuickPick()); + picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat"); + picker.items = getPicks(); + store.add(picker.onDidTriggerItemButton(context => { + if (context.button === openInEditorButton) { + const options: IChatEditorOptions = { target: { sessionId: context.item.chat.sessionId }, pinned: true }; + editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, ACTIVE_GROUP); + picker.hide(); + } else if (context.button === deleteButton) { + chatService.removeHistoryEntry(context.item.chat.sessionId); + picker.items = getPicks(); + } + })); + store.add(picker.onDidAccept(async () => { + try { + const item = picker.selectedItems[0]; + const sessionId = item.chat.sessionId; + const view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; + view.loadSession(sessionId); + } finally { + picker.hide(); + } + })); + store.add(picker.onDidHide(() => store.dispose())); + + picker.show(); } } -export class ChatSubmitEditorAction extends EditorAction2 { - static readonly ID = 'workbench.action.chat.acceptInput'; - +class OpenChatEditorAction extends Action2 { constructor() { super({ - id: ChatSubmitEditorAction.ID, - title: localize2({ key: 'actions.chat.submit', comment: ['Apply input from the chat input box'] }, "Submit"), - precondition: CONTEXT_CHAT_INPUT_HAS_TEXT, - keybinding: { - when: CONTEXT_IN_CHAT_INPUT, - primary: KeyCode.Enter, - weight: KeybindingWeight.EditorContrib - }, - menu: { - id: MenuId.ChatExecuteSecondary, - when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), - group: 'group_1', - }, + id: `workbench.action.openChat`, + title: localize2('interactiveSession.open', "Open Editor"), + f1: true, + category: CHAT_CATEGORY, + precondition: CONTEXT_CHAT_ENABLED }); } - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { - const editorUri = editor.getModel()?.uri; - if (editorUri) { - const widgetService = accessor.get(IChatWidgetService); - widgetService.getWidgetByInputUri(editorUri)?.acceptInput(); - } + async run(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } satisfies IChatEditorOptions }); } } export function registerChatActions() { registerAction2(OpenChatGlobalAction); - registerAction2(ChatSubmitEditorAction); - - registerAction2(ChatSubmitSecondaryAgentEditorAction); + registerAction2(ChatHistoryAction); + registerAction2(OpenChatEditorAction); registerAction2(class ClearChatInputHistoryAction extends Action2 { constructor() { super({ id: 'workbench.action.chat.clearInputHistory', title: localize2('interactiveSession.clearHistory.label', "Clear Input History"), - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, category: CHAT_CATEGORY, f1: true, }); @@ -195,14 +224,32 @@ export function registerChatActions() { super({ id: 'workbench.action.chat.clearHistory', title: localize2('chat.clear.label', "Clear All Workspace Chats"), - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, category: CHAT_CATEGORY, f1: true, }); } async run(accessor: ServicesAccessor, ...args: any[]) { + const editorGroupsService = accessor.get(IEditorGroupsService); + const viewsService = accessor.get(IViewsService); + const chatService = accessor.get(IChatService); chatService.clearAllHistoryEntries(); + + const chatView = viewsService.getViewWithId(CHAT_VIEW_ID) as ChatViewPane | undefined; + if (chatView) { + chatView.widget.clear(); + } + + // Clear all chat editors. Have to go this route because the chat editor may be in the background and + // not have a ChatEditorInput. + editorGroupsService.groups.forEach(group => { + group.editors.forEach(editor => { + if (editor instanceof ChatEditorInput) { + clearChatEditor(accessor, editor); + } + }); + }); } }); @@ -211,7 +258,7 @@ export function registerChatActions() { super({ id: 'chat.action.focus', title: localize2('actions.interactiveSession.focus', 'Focus Chat List'), - precondition: CONTEXT_IN_CHAT_INPUT, + precondition: ContextKeyExpr.and(CONTEXT_IN_CHAT_INPUT), category: CHAT_CATEGORY, keybinding: [ // On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top @@ -239,20 +286,6 @@ export function registerChatActions() { } }); - class ChatAccessibilityHelpContribution extends Disposable { - static ID: 'chatAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(105, 'panelChat', async accessor => { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - runAccessibilityHelpAction(accessor, codeEditor ?? undefined, 'panelChat'); - }, ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST))); - } - } - - const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); - workbenchRegistry.registerWorkbenchContribution(ChatAccessibilityHelpContribution, LifecyclePhase.Eventually); - registerAction2(class FocusChatInputAction extends Action2 { constructor() { super({ @@ -273,78 +306,10 @@ export function registerChatActions() { }); } -export function getOpenChatEditorAction(id: string, label: string, when?: string) { - return class OpenChatEditor extends Action2 { - constructor() { - super({ - id: `workbench.action.openChat.${id}`, - title: localize2('interactiveSession.open', "Open Editor ({0})", label), - f1: true, - category: CHAT_CATEGORY, - precondition: ContextKeyExpr.deserialize(when) - }); - } - - async run(accessor: ServicesAccessor) { - const editorService = accessor.get(IEditorService); - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { providerId: id }, pinned: true } }); - } - }; -} - -const getHistoryChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly & { viewId: string } => ({ - viewId, - id: `workbench.action.chat.${providerId}.history`, - title: localize2('chat.history.label', "Show Chats"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', viewId), - group: 'navigation', - order: -1 - }, - category: CHAT_CATEGORY, - icon: Codicon.history, - f1: true, - precondition: CONTEXT_PROVIDER_EXISTS -}); - -export function getHistoryAction(viewId: string, providerId: string) { - return class HistoryAction extends ViewAction { - constructor() { - super(getHistoryChatActionDescriptorForViewTitle(viewId, providerId)); - } - - async runInView(accessor: ServicesAccessor, view: ChatViewPane) { - const chatService = accessor.get(IChatService); - const quickInputService = accessor.get(IQuickInputService); - const chatContribService = accessor.get(IChatContributionService); - const viewsService = accessor.get(IViewsService); - const items = chatService.getHistory(); - const picks = items.map(i => ({ - label: i.title, - chat: i, - buttons: [{ - iconClass: ThemeIcon.asClassName(Codicon.x), - tooltip: localize('interactiveSession.history.delete', "Delete"), - }] - })); - const selection = await quickInputService.pick(picks, - { - placeHolder: localize('interactiveSession.history.pick', "Switch to chat"), - onDidTriggerItemButton: context => { - chatService.removeHistoryEntry(context.item.chat.sessionId); - context.removeItem(); - } - }); - if (selection) { - const sessionId = selection.chat.sessionId; - const provider = chatContribService.registeredProviders[0]?.id; - if (provider) { - const viewId = chatContribService.getViewIdForProvider(provider); - const view = await viewsService.openView(viewId) as ChatViewPane; - view.loadSession(sessionId); - } - } - } - }; +export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { + if (isRequestVM(item)) { + return (includeName ? `${item.username}: ` : '') + item.messageText; + } else { + return (includeName ? `${item.username}: ` : '') + item.response.toString(); + } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts index 8edda6bd059..cd85340c42a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts @@ -6,18 +6,22 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -export async function clearChatEditor(accessor: ServicesAccessor): Promise { +export async function clearChatEditor(accessor: ServicesAccessor, chatEditorInput?: ChatEditorInput): Promise { const editorService = accessor.get(IEditorService); - const editorGroupsService = accessor.get(IEditorGroupsService); - const chatEditorInput = editorService.activeEditor; - if (chatEditorInput instanceof ChatEditorInput && chatEditorInput.providerId) { + if (!chatEditorInput) { + const editorInput = editorService.activeEditor; + chatEditorInput = editorInput instanceof ChatEditorInput ? editorInput : undefined; + } + + if (chatEditorInput instanceof ChatEditorInput) { + // A chat editor can only be open in one group + const identifier = editorService.findEditors(chatEditorInput.resource)[0]; await editorService.replaceEditors([{ editor: chatEditorInput, - replacement: { resource: ChatEditorInput.getNewEditorUri(), options: { target: { providerId: chatEditorInput.providerId, pinned: true } } } - }], editorGroupsService.activeGroup); + replacement: { resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } satisfies IChatEditorOptions } + }], identifier.groupId); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 2736da158c5..1c6572f3415 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -7,23 +7,22 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize2 } from 'vs/nls'; -import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CHAT_CATEGORY, isChatViewTitleActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CHAT_VIEW_ID, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`; export function registerNewChatActions() { - registerAction2(class NewChatEditorAction extends Action2 { constructor() { super({ @@ -31,7 +30,7 @@ export function registerNewChatActions() { title: localize2('chat.newChat.label', "New Chat"), icon: Codicon.plus, f1: false, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, menu: [{ id: MenuId.EditorTitle, group: 'navigation', @@ -41,12 +40,11 @@ export function registerNewChatActions() { }); } async run(accessor: ServicesAccessor, ...args: any[]) { - announceChatCleared(accessor); + announceChatCleared(accessor.get(IAccessibilitySignalService)); await clearChatEditor(accessor); } }); - registerAction2(class GlobalClearChatAction extends Action2 { constructor() { super({ @@ -54,7 +52,7 @@ export function registerNewChatActions() { title: localize2('chat.newChat.label', "New Chat"), category: CHAT_CATEGORY, icon: Codicon.plus, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, @@ -64,58 +62,46 @@ export function registerNewChatActions() { }, when: CONTEXT_IN_CHAT_SESSION }, - menu: { + menu: [{ id: MenuId.ChatContext, group: 'z_clear' - } + }, + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), + group: 'navigation', + order: -1 + }] }); } - run(accessor: ServicesAccessor, ...args: any[]) { - const widgetService = accessor.get(IChatWidgetService); + async run(accessor: ServicesAccessor, ...args: any[]) { + const context = args[0]; + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); + if (isChatViewTitleActionContext(context)) { + // Is running in the Chat view title + announceChatCleared(accessibilitySignalService); + context.chatView.widget.clear(); + context.chatView.widget.focusInput(); + } else { + // Is running from f1 or keybinding + const widgetService = accessor.get(IChatWidgetService); + const viewsService = accessor.get(IViewsService); - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return; + let widget = widgetService.lastFocusedWidget; + if (!widget) { + const chatView = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; + widget = chatView.widget; + } + + announceChatCleared(accessibilitySignalService); + widget.clear(); + widget.focusInput(); } - - announceChatCleared(accessor); - widget.clear(); - widget.focusInput(); } }); } -const getNewChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly & { viewId: string } => ({ - viewId, - id: `workbench.action.chat.${providerId}.newChat`, - title: localize2('chat.newChat.label', "New Chat"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', viewId), - group: 'navigation', - order: -1 - }, - precondition: CONTEXT_PROVIDER_EXISTS, - category: CHAT_CATEGORY, - icon: Codicon.plus, - f1: false -}); - -export function getNewChatAction(viewId: string, providerId: string) { - return class NewChatAction extends ViewAction { - constructor() { - super(getNewChatActionDescriptorForViewTitle(viewId, providerId)); - } - - async runInView(accessor: ServicesAccessor, view: ChatViewPane) { - announceChatCleared(accessor); - await view.clear(); - view.widget.focusInput(); - } - }; -} - -function announceChatCleared(accessor: ServicesAccessor): void { - accessor.get(IAccessibilitySignalService).playSignal(AccessibilitySignal.clear); +function announceChatCleared(accessibilitySignalService: IAccessibilitySignalService): void { + accessibilitySignalService.playSignal(AccessibilitySignal.clear); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 3cd6e415c9c..0034369dcdf 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -6,9 +6,9 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -17,18 +17,21 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; -import { localize2 } from 'vs/nls'; +import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService, IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; -import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_EDIT_APPLIED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatCopyKind, IChatService, IDocumentContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; @@ -37,6 +40,8 @@ import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import * as strings from 'vs/base/common/strings'; +import { CharCode } from 'vs/base/common/charCode'; export interface IChatCodeBlockActionContext extends ICodeBlockActionContext { element: IChatResponseViewModel; @@ -46,6 +51,10 @@ export function isCodeBlockActionContext(thing: unknown): thing is ICodeBlockAct return typeof thing === 'object' && thing !== null && 'code' in thing && 'element' in thing; } +export function isCodeCompareBlockActionContext(thing: unknown): thing is ICodeCompareBlockActionContext { + return typeof thing === 'object' && thing !== null && 'element' in thing; +} + function isResponseFiltered(context: ICodeBlockActionContext) { return isResponseVM(context.element) && context.element.errorDetails?.responseIsFiltered; } @@ -76,6 +85,168 @@ abstract class ChatCodeBlockAction extends Action2 { abstract runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext): any; } +abstract class InsertCodeBlockAction extends ChatCodeBlockAction { + + override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { + const editorService = accessor.get(IEditorService); + const textFileService = accessor.get(ITextFileService); + + if (isResponseFiltered(context)) { + // When run from command palette + return; + } + + if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { + return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); + } + + let activeEditorControl = editorService.activeTextEditorControl; + if (isDiffEditor(activeEditorControl)) { + activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); + } + + if (!isCodeEditor(activeEditorControl)) { + return; + } + + if (!activeEditorControl.hasModel()) { + return; + } + const activeModelUri = activeEditorControl.getModel().uri; + + // Check if model is editable, currently only support untitled and text file + const activeTextModel = textFileService.files.get(activeModelUri) ?? textFileService.untitled.get(activeModelUri); + if (!activeTextModel || activeTextModel.isReadonly()) { + return; + } + + await this.handleTextEditor(accessor, activeEditorControl, context); + } + + private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { + if (!notebookEditor.hasModel()) { + return; + } + + if (notebookEditor.isReadOnly) { + return; + } + + if (notebookEditor.activeCodeEditor?.hasTextFocus()) { + const codeEditor = notebookEditor.activeCodeEditor; + if (codeEditor.hasModel()) { + return this.handleTextEditor(accessor, codeEditor, context); + } + } + + const languageService = accessor.get(ILanguageService); + const focusRange = notebookEditor.getFocus(); + const next = Math.max(focusRange.end - 1, 0); + insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); + this.notifyUserAction(accessor, context); + } + + protected async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + const activeModel = codeEditor.getModel(); + const range = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); + const text = reindent(codeBlockActionContext.code, activeModel, range.startLineNumber); + return [new ResourceTextEdit(activeModel.uri, { range, text })]; + } + + private async handleTextEditor(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { + const bulkEditService = accessor.get(IBulkEditService); + const codeEditorService = accessor.get(ICodeEditorService); + + this.notifyUserAction(accessor, codeBlockActionContext); + const activeModel = codeEditor.getModel(); + + const mappedEdits = await this.computeEdits(accessor, codeEditor, codeBlockActionContext); + + await bulkEditService.apply(mappedEdits); + codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); + } + + private notifyUserAction(accessor: ServicesAccessor, context: ICodeBlockActionContext) { + if (isResponseVM(context.element)) { + const chatService = accessor.get(IChatService); + chatService.notifyUserAction({ + agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, + sessionId: context.element.sessionId, + requestId: context.element.requestId, + result: context.element.result, + action: { + kind: 'insert', + codeBlockIndex: context.codeBlockIndex, + totalCharacters: context.code.length, + } + }); + } + } + +} + +function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number) { + const newContent = strings.splitLines(codeBlockContent); + if (newContent.length === 0) { + return codeBlockContent; + } + + const formattingOptions = model.getFormattingOptions(); + const codeIndentLevel = computeIndentation(model.getLineContent(seletionStartLine), formattingOptions.tabSize).level; + + const indents = newContent.map(line => computeIndentation(line, formattingOptions.tabSize)); + + // find the smallest indent level in the code block + const newContentIndentLevel = indents.reduce((min, indent, index) => { + if (indent.length !== newContent[index].length) { // ignore empty lines + return Math.min(indent.level, min); + } + return min; + }, Number.MAX_VALUE); + + if (newContentIndentLevel === Number.MAX_VALUE || newContentIndentLevel === codeIndentLevel) { + // all lines are empty or the indent is already correct + return codeBlockContent; + } + const newLines = []; + for (let i = 0; i < newContent.length; i++) { + const { level, length } = indents[i]; + const newLevel = Math.max(0, codeIndentLevel + level - newContentIndentLevel); + const newIndentation = formattingOptions.insertSpaces ? ' '.repeat(formattingOptions.tabSize * newLevel) : '\t'.repeat(newLevel); + newLines.push(newIndentation + newContent[i].substring(length)); + } + return newLines.join('\n'); +} + +// TODO: Merge with `computeIndentLevel` from `vs/editor/common/model/utils.ts` +function computeIndentation(line: string, tabSize: number): { level: number; length: number } { + let nSpaces = 0; + let level = 0; + let i = 0; + let length = 0; + const len = line.length; + while (i < len) { + const chCode = line.charCodeAt(i); + if (chCode === CharCode.Space) { + nSpaces++; + if (nSpaces === tabSize) { + level++; + nSpaces = 0; + length = i + 1; + } + } else if (chCode === CharCode.Tab) { + level++; + nSpaces = 0; + length = i + 1; + } else { + break; + } + i++; + } + return { level, length }; +} + export function registerChatCodeBlockActions() { registerAction2(class CopyCodeBlockAction extends Action2 { constructor() { @@ -87,7 +258,8 @@ export function registerChatCodeBlockActions() { icon: Codicon.copy, menu: { id: MenuId.ChatCodeBlock, - group: 'navigation' + group: 'navigation', + order: 30 } }); } @@ -104,8 +276,8 @@ export function registerChatCodeBlockActions() { if (isResponseVM(context.element)) { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ - providerId: context.element.providerId, agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, sessionId: context.element.sessionId, requestId: context.element.requestId, result: context.element.result, @@ -150,8 +322,8 @@ export function registerChatCodeBlockActions() { const element = context.element as IChatResponseViewModel | undefined; if (element) { chatService.notifyUserAction({ - providerId: element.providerId, agentId: element.agent?.id, + command: element.slashCommand?.name, sessionId: element.sessionId, requestId: element.requestId, result: element.result, @@ -175,19 +347,20 @@ export function registerChatCodeBlockActions() { return false; }); - registerAction2(class InsertCodeBlockAction extends ChatCodeBlockAction { + registerAction2(class SmartApplyInEditorAction extends InsertCodeBlockAction { constructor() { super({ - id: 'workbench.action.chat.insertCodeBlock', - title: localize2('interactive.insertCodeBlock.label', "Insert at Cursor"), - precondition: CONTEXT_PROVIDER_EXISTS, + id: 'workbench.action.chat.applyInEditor', + title: localize2('interactive.applyInEditor.label', "Apply in Editor"), + precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, - icon: Codicon.insert, + icon: Codicon.sparkle, menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - when: CONTEXT_IN_CHAT_SESSION + when: CONTEXT_IN_CHAT_SESSION, + order: 10 }, keybinding: { when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), @@ -198,148 +371,96 @@ export function registerChatCodeBlockActions() { }); } - override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - const editorService = accessor.get(IEditorService); - const textFileService = accessor.get(ITextFileService); + protected override async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - if (isResponseFiltered(context)) { - // When run from command palette - return; - } + const progressService = accessor.get(IProgressService); + const notificationService = accessor.get(INotificationService); - if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); - } - - let activeEditorControl = editorService.activeTextEditorControl; - if (isDiffEditor(activeEditorControl)) { - activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); - } - - if (!isCodeEditor(activeEditorControl)) { - return; - } - - const activeModel = activeEditorControl.getModel(); - if (!activeModel) { - return; - } - - // Check if model is editable, currently only support untitled and text file - const activeTextModel = textFileService.files.get(activeModel.uri) ?? textFileService.untitled.get(activeModel.uri); - if (!activeTextModel || activeTextModel.isReadonly()) { - return; - } - - await this.handleTextEditor(accessor, activeEditorControl, activeModel, context); - } - - private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { - if (!notebookEditor.hasModel()) { - return; - } - - if (notebookEditor.isReadOnly) { - return; - } - - if (notebookEditor.activeCodeEditor?.hasTextFocus()) { - const codeEditor = notebookEditor.activeCodeEditor; - const textModel = codeEditor.getModel(); - - if (textModel) { - return this.handleTextEditor(accessor, codeEditor, textModel, context); - } - } - - const languageService = accessor.get(ILanguageService); - const focusRange = notebookEditor.getFocus(); - const next = Math.max(focusRange.end - 1, 0); - insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); - this.notifyUserAction(accessor, context); - } - - private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, codeBlockActionContext: ICodeBlockActionContext) { - this.notifyUserAction(accessor, codeBlockActionContext); - - const bulkEditService = accessor.get(IBulkEditService); - const codeEditorService = accessor.get(ICodeEditorService); + const activeModel = codeEditor.getModel(); const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); - - // try applying workspace edit that was returned by a MappedEditsProvider, else simply insert at selection - - let mappedEdits: WorkspaceEdit | null = null; - if (mappedEditsProviders.length > 0) { - const mostRelevantProvider = mappedEditsProviders[0]; // TODO@ulugbekna: should we try all providers? // 0th sub-array - editor selections array if there are any selections // 1st sub-array - array with documents used to get the chat reply const docRefs: DocumentContextItem[][] = []; - if (codeEditor.hasModel()) { - const model = codeEditor.getModel(); - const currentDocUri = model.uri; - const currentDocVersion = model.getVersionId(); - const selections = codeEditor.getSelections(); - if (selections.length > 0) { - docRefs.push([ - { - uri: currentDocUri, - version: currentDocVersion, - ranges: selections, - } - ]); - } + const currentDocUri = activeModel.uri; + const currentDocVersion = activeModel.getVersionId(); + const selections = codeEditor.getSelections(); + if (selections.length > 0) { + docRefs.push([ + { + uri: currentDocUri, + version: currentDocVersion, + ranges: selections, + } + ]); } - const usedDocuments = getUsedDocuments(codeBlockActionContext); if (usedDocuments) { docRefs.push(usedDocuments); } const cancellationTokenSource = new CancellationTokenSource(); - - mappedEdits = await mostRelevantProvider.provideMappedEdits( - activeModel, - [codeBlockActionContext.code], - { documents: docRefs }, - cancellationTokenSource.token); - } - - if (mappedEdits) { - await bulkEditService.apply(mappedEdits); - } else { - const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); - await bulkEditService.apply([ - new ResourceTextEdit(activeModel.uri, { - range: activeSelection, - text: codeBlockActionContext.code, - }), - ]); - } - codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); - } - - private notifyUserAction(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - if (isResponseVM(context.element)) { - const chatService = accessor.get(IChatService); - chatService.notifyUserAction({ - providerId: context.element.providerId, - agentId: context.element.agent?.id, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'insert', - codeBlockIndex: context.codeBlockIndex, - totalCharacters: context.code.length, + try { + const edits = await progressService.withProgress( + { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, + async progress => { + for (const provider of mappedEditsProviders) { + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", provider.displayName) }); + const mappedEdits = await provider.provideMappedEdits( + activeModel, + [codeBlockActionContext.code], + { documents: docRefs }, + cancellationTokenSource.token + ); + if (mappedEdits) { + return mappedEdits; + } + } + return undefined; + }, + () => cancellationTokenSource.cancel() + ); + if (edits) { + return edits; } - }); - } - } + } catch (e) { + notificationService.notify({ severity: Severity.Error, message: localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message) }); + } finally { + cancellationTokenSource.dispose(); + } + } + // fall back to inserting the code block as is + return super.computeEdits(accessor, codeEditor, codeBlockActionContext); + } + }); + + registerAction2(class SmartApplyInEditorAction extends InsertCodeBlockAction { + constructor() { + super({ + id: 'workbench.action.chat.insertCodeBlock', + title: localize2('interactive.insertCodeBlock.label', "Insert At Cursor"), + precondition: CONTEXT_CHAT_ENABLED, + f1: true, + category: CHAT_CATEGORY, + icon: Codicon.insert, + menu: { + id: MenuId.ChatCodeBlock, + group: 'navigation', + when: CONTEXT_IN_CHAT_SESSION, + order: 20 + }, + keybinding: { + when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), + primary: KeyMod.CtrlCmd | KeyCode.Enter, + mac: { primary: KeyMod.WinCtrl | KeyCode.Enter }, + weight: KeybindingWeight.ExternalExtension + 1 + }, + }); + } }); registerAction2(class InsertIntoNewFileAction extends ChatCodeBlockAction { @@ -347,14 +468,15 @@ export function registerChatCodeBlockActions() { super({ id: 'workbench.action.chat.insertIntoNewFile', title: localize2('interactive.insertIntoNewFile.label', "Insert into New File"), - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, icon: Codicon.newFile, menu: { id: MenuId.ChatCodeBlock, group: 'navigation', - isHiddenByDefault: true + isHiddenByDefault: true, + order: 40, } }); } @@ -368,12 +490,12 @@ export function registerChatCodeBlockActions() { const editorService = accessor.get(IEditorService); const chatService = accessor.get(IChatService); - editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined }); + editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined } satisfies IUntitledTextResourceEditorInput); if (isResponseVM(context.element)) { chatService.notifyUserAction({ - providerId: context.element.providerId, agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, sessionId: context.element.sessionId, requestId: context.element.requestId, result: context.element.result, @@ -402,7 +524,7 @@ export function registerChatCodeBlockActions() { super({ id: 'workbench.action.chat.runInTerminal', title: localize2('interactive.runInTerminal.label', "Insert into Terminal"), - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, icon: Codicon.terminal, @@ -465,8 +587,8 @@ export function registerChatCodeBlockActions() { if (isResponseVM(context.element)) { chatService.notifyUserAction({ - providerId: context.element.providerId, agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, sessionId: context.element.sessionId, requestId: context.element.requestId, result: context.element.result, @@ -497,7 +619,7 @@ export function registerChatCodeBlockActions() { const currentResponse = curCodeBlockInfo ? curCodeBlockInfo.element : (focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item))); - if (!currentResponse) { + if (!currentResponse || !isResponseVM(currentResponse)) { return; } @@ -521,7 +643,7 @@ export function registerChatCodeBlockActions() { weight: KeybindingWeight.WorkbenchContrib, when: CONTEXT_IN_CHAT_SESSION, }, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, }); @@ -543,7 +665,7 @@ export function registerChatCodeBlockActions() { weight: KeybindingWeight.WorkbenchContrib, when: CONTEXT_IN_CHAT_SESSION, }, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, }); @@ -582,3 +704,76 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): languageId: editor.getModel()!.getLanguageId(), }; } + +export function registerChatCodeCompareBlockActions() { + + abstract class ChatCompareCodeBlockAction extends Action2 { + run(accessor: ServicesAccessor, ...args: any[]) { + const context = args[0]; + if (!isCodeCompareBlockActionContext(context)) { + return; + // TODO@jrieken derive context + } + + return this.runWithContext(accessor, context); + } + + abstract runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): any; + } + + registerAction2(class ApplyEditsCompareBlockAction extends ChatCompareCodeBlockAction { + constructor() { + super({ + id: 'workbench.action.chat.applyCompareEdits', + title: localize2('interactive.compare.apply', "Apply Edits"), + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.check, + precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), + menu: { + id: MenuId.ChatCompareBlock, + group: 'navigation', + order: 1, + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { + + const editorService = accessor.get(IEditorService); + const instaService = accessor.get(IInstantiationService); + + const editor = instaService.createInstance(DefaultChatTextEditor); + await editor.apply(context.element, context.edit, context.diffEditor); + + await editorService.openEditor({ + resource: context.edit.uri, + options: { revealIfVisible: true }, + }); + } + }); + + registerAction2(class DiscardEditsCompareBlockAction extends ChatCompareCodeBlockAction { + constructor() { + super({ + id: 'workbench.action.chat.discardCompareEdits', + title: localize2('interactive.compare.discard', "Discard Edits"), + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.trash, + precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), + menu: { + id: MenuId.ChatCompareBlock, + group: 'navigation', + order: 2, + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { + const instaService = accessor.get(IInstantiationService); + const editor = instaService.createInstance(DefaultChatTextEditor); + editor.discard(context.element, context.edit); + } + }); +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts new file mode 100644 index 00000000000..0f65c731173 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -0,0 +1,398 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Schemas } from 'vs/base/common/network'; +import { compare } from 'vs/base/common/strings'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { IRange } from 'vs/editor/common/core/range'; +import { EditorType } from 'vs/editor/common/editorCommon'; +import { Command } from 'vs/editor/common/languages'; +import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess'; +import { localize, localize2 } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_QUICK_CHAT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; +import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export function registerChatContextActions() { + registerAction2(AttachContextAction); + registerAction2(AttachFileAction); + registerAction2(AttachSelectionAction); +} + +export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem; + +export interface IFileQuickPickItem extends IQuickPickItem { + kind: 'file'; + id: string; + name: string; + value: URI; + isDynamic: true; + + resource: URI; +} + +export interface IDynamicVariableQuickPickItem extends IQuickPickItem { + kind: 'dynamic'; + id: string; + name?: string; + value: unknown; + isDynamic: true; + + icon?: ThemeIcon; + command?: Command; +} + +export interface IToolQuickPickItem extends IQuickPickItem { + kind: 'tool'; + id: string; + name?: string; +} + +export interface IStaticVariableQuickPickItem extends IQuickPickItem { + kind: 'static'; + id: string; + name: string; + value: unknown; + isDynamic?: false; + + icon?: ThemeIcon; +} + +export interface IQuickAccessQuickPickItem extends IQuickPickItem { + kind: 'quickaccess'; + id: string; + name: string; + value: string; + + prefix: string; +} + +class AttachFileAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachFile'; + + constructor() { + super({ + id: AttachFileAction.ID, + title: localize2('workbench.action.chat.attachFile.label', "Attach File"), + category: CHAT_CATEGORY, + f1: false + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const variablesService = accessor.get(IChatVariablesService); + const textEditorService = accessor.get(IEditorService); + + const activeUri = textEditorService.activeEditor?.resource; + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + variablesService.attachContext('file', activeUri, ChatAgentLocation.Panel); + } + } +} + +class AttachSelectionAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachSelection'; + + constructor() { + super({ + id: AttachSelectionAction.ID, + title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), + category: CHAT_CATEGORY, + f1: false + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const variablesService = accessor.get(IChatVariablesService); + const textEditorService = accessor.get(IEditorService); + + const activeEditor = textEditorService.activeTextEditorControl; + const activeUri = textEditorService.activeEditor?.resource; + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + const selection = activeEditor?.getSelection(); + if (selection) { + variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); + } + } + } +} + +class AttachContextAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachContext'; + + // used to enable/disable the keybinding and defined menu containment + private static _cdt = ContextKeyExpr.or( + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), CONTEXT_IN_QUICK_CHAT.isEqualTo(false)), + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor), ContextKeyExpr.equals('config.chat.experimental.variables.editor', true)), + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Notebook), ContextKeyExpr.equals('config.chat.experimental.variables.notebook', true)), + ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Terminal), ContextKeyExpr.equals('config.chat.experimental.variables.terminal', true)), + ); + + constructor() { + super({ + id: AttachContextAction.ID, + title: localize2('workbench.action.chat.attachContext.label', "Attach Context"), + icon: Codicon.attach, + category: CHAT_CATEGORY, + precondition: AttachContextAction._cdt, + keybinding: { + when: CONTEXT_IN_CHAT_INPUT, + primary: KeyMod.CtrlCmd | KeyCode.Slash, + weight: KeybindingWeight.EditorContrib + }, + menu: [ + { + when: AttachContextAction._cdt, + id: MenuId.ChatExecute, + group: 'navigation', + }, + ] + }); + } + + private _getFileContextId(item: { resource: URI } | { uri: URI; range: IRange }) { + if ('resource' in item) { + return item.resource.toString(); + } + + return item.uri.toString() + (item.range.startLineNumber !== item.range.endLineNumber ? + `:${item.range.startLineNumber}-${item.range.endLineNumber}` : + `:${item.range.startLineNumber}`); + } + + private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: IChatContextQuickPickItem[]) { + const toAttach: IChatRequestVariableEntry[] = []; + for (const pick of picks) { + if (pick && typeof pick === 'object' && 'command' in pick && pick.command) { + // Dynamic variable with a followup command + const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); + if (!selection) { + // User made no selection, skip this variable + continue; + } + toAttach.push({ + ...pick, + isDynamic: pick.isDynamic, + value: pick.value, + name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, + // Apply the original icon with the new name + fullName: selection + }); + } else if ('symbol' in pick && pick.symbol) { + // Symbol + toAttach.push({ + ...pick, + id: this._getFileContextId(pick.symbol.location), + value: pick.symbol.location, + fullName: pick.label, + name: pick.symbol.name, + isDynamic: true + }); + } else if (pick && typeof pick === 'object' && 'resource' in pick && pick.resource) { + // #file variable + toAttach.push({ + ...pick, + id: this._getFileContextId({ resource: pick.resource }), + value: pick.resource, + name: pick.label, + isFile: true, + isDynamic: true + }); + } else if ('symbolName' in pick && pick.uri && pick.range) { + // Symbol + toAttach.push({ + ...pick, + range: undefined, + id: this._getFileContextId({ uri: pick.uri, range: pick.range.decoration }), + value: { uri: pick.uri, range: pick.range.decoration }, + fullName: pick.label, + name: pick.symbolName!, + isDynamic: true + }); + } else if ('kind' in pick && pick.kind === 'tool') { + toAttach.push({ + id: pick.id, + name: pick.label, + fullName: pick.label, + value: undefined, + isTool: true + }); + } else { + // All other dynamic variables and static variables + toAttach.push({ + ...pick, + range: undefined, + id: pick.id ?? '', + value: 'value' in pick ? pick.value : undefined, + fullName: pick.label, + name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label, + icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined + }); + } + } + + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, ...toAttach); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const quickInputService = accessor.get(IQuickInputService); + const chatAgentService = accessor.get(IChatAgentService); + const chatVariablesService = accessor.get(IChatVariablesService); + const commandService = accessor.get(ICommandService); + const widgetService = accessor.get(IChatWidgetService); + const languageModelToolsService = accessor.get(ILanguageModelToolsService); + const context: { widget?: IChatWidget } | undefined = args[0]; + const widget = context?.widget ?? widgetService.lastFocusedWidget; + if (!widget) { + return; + } + + const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); + const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; + const quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] = []; + for (const variable of chatVariablesService.getVariables(widget.location)) { + if (variable.fullName && (!variable.isSlow || slowSupported)) { + quickPickItems.push({ + label: variable.fullName, + name: variable.name, + id: variable.id, + iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, + icon: variable.icon + }); + } + } + + if (widget.viewModel?.sessionId) { + const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); + if (agentPart) { + const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); + for (const variable of completions) { + if (variable.fullName) { + quickPickItems.push({ + label: variable.fullName, + id: variable.id, + command: variable.command, + icon: variable.icon, + iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, + value: variable.value, + isDynamic: true, + name: variable.name + }); + } + } + } + } + + for (const tool of languageModelToolsService.getTools()) { + if (tool.canBeInvokedManually) { + const item: IToolQuickPickItem = { + kind: 'tool', + label: tool.displayName ?? tool.name, + id: tool.name, + }; + if (ThemeIcon.isThemeIcon(tool.icon)) { + item.iconClass = ThemeIcon.asClassName(tool.icon); + } else if (tool.icon) { + item.iconPath = tool.icon; + } + + quickPickItems.push(item); + } + } + + quickPickItems.push({ + label: localize('chatContext.symbol', 'Symbol...'), + icon: ThemeIcon.fromId(Codicon.symbolField.id), + iconClass: ThemeIcon.asClassName(Codicon.symbolField), + prefix: SymbolsQuickAccessProvider.PREFIX + }); + + function extractTextFromIconLabel(label: string | undefined): string { + if (!label) { + return ''; + } + const match = label.match(/\$\([^\)]+\)\s*(.+)/); + return match ? match[1] : label; + } + + this._show(quickInputService, commandService, widget, quickPickItems.sort(function (a, b) { + + const first = extractTextFromIconLabel(a.label).toUpperCase(); + const second = extractTextFromIconLabel(b.label).toUpperCase(); + + return compare(first, second); + })); + } + + private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[], query: string = '') { + + quickInputService.quickAccess.show(query, { + enabledProviderPrefixes: [ + AnythingQuickAccessProvider.PREFIX, + SymbolsQuickAccessProvider.PREFIX, + AbstractGotoSymbolQuickAccessProvider.PREFIX + ], + placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), + providerOptions: { + handleAccept: (item: IChatContextQuickPickItem) => { + if ('prefix' in item) { + this._show(quickInputService, commandService, widget, quickPickItems, item.prefix); + } else { + this._attachContext(widget, commandService, item); + } + }, + additionPicks: quickPickItems, + filter: (item: IChatContextQuickPickItem) => { + // Avoid attaching the same context twice + const attachedContext = widget.getContrib(ChatContextAttachments.ID)?.getContext() ?? new Set(); + + if ('symbol' in item && item.symbol) { + return !attachedContext.has(this._getFileContextId(item.symbol.location)); + } + + if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { + return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme) + && !attachedContext.has(this._getFileContextId({ resource: item.resource })); // Hack because Typescript doesn't narrow this type correctly + } + + if (item && typeof item === 'object' && 'uri' in item && item.uri && item.range) { + return !attachedContext.has(this._getFileContextId({ uri: item.uri, range: item.range.decoration })); + } + + if (!('command' in item) && item.id) { + return !attachedContext.has(item.id); + } + + // Don't filter out dynamic variables which show secondary data (temporary) + return true; + } + } + }); + + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index c55739a639a..5504c8e556f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -7,7 +7,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CHAT_CATEGORY, stringifyItem } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -72,11 +72,3 @@ export function registerChatCopyActions() { } }); } - -function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { - if (isRequestVM(item)) { - return (includeName ? `${item.username}: ` : '') + item.messageText; - } else { - return (includeName ? `${item.username}: ` : '') + item.response.asString(); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts new file mode 100644 index 00000000000..4ab735e2b4e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; + +export function registerChatDeveloperActions() { + registerAction2(LogChatInputHistoryAction); +} + +class LogChatInputHistoryAction extends Action2 { + + static readonly ID = 'workbench.action.chat.logInputHistory'; + + constructor() { + super({ + id: LogChatInputHistoryAction.ID, + title: localize2('workbench.action.chat.logInputHistory.label', "Log Chat Input History"), + icon: Codicon.attach, + category: Categories.Developer, + f1: true + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + chatWidgetService.lastFocusedWidget?.logInputHistory(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 4888f64fa5a..b7d76a5a227 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -12,7 +12,9 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { chatAgentLeader, extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; export interface IVoiceChatExecuteActionContext { @@ -31,16 +33,27 @@ export class SubmitAction extends Action2 { constructor() { super({ id: SubmitAction.ID, - title: localize2('interactive.submit.label', "Submit"), + title: localize2('interactive.submit.label', "Send"), f1: false, category: CHAT_CATEGORY, icon: Codicon.send, precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), - menu: { - id: MenuId.ChatExecute, - when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), - group: 'navigation', + keybinding: { + when: CONTEXT_IN_CHAT_INPUT, + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib }, + menu: [ + { + id: MenuId.ChatExecuteSecondary, + group: 'group_1', + }, + { + id: MenuId.ChatExecute, + when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), + group: 'navigation', + }, + ] }); } @@ -53,6 +66,50 @@ export class SubmitAction extends Action2 { } } + +export class ChatSubmitSecondaryAgentAction extends Action2 { + static readonly ID = 'workbench.action.chat.submitSecondaryAgent'; + + constructor() { + super({ + id: ChatSubmitSecondaryAgentAction.ID, + title: localize2({ key: 'actions.chat.submitSecondaryAgent', comment: ['Send input from the chat input box to the secondary agent'] }, "Submit to Secondary Agent"), + precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_INPUT_HAS_AGENT.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + keybinding: { + when: CONTEXT_IN_CHAT_INPUT, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + }, + menu: { + id: MenuId.ChatExecuteSecondary, + group: 'group_1' + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const context: IChatExecuteActionContext | undefined = args[0]; + const agentService = accessor.get(IChatAgentService); + const secondaryAgent = agentService.getSecondaryAgent(); + if (!secondaryAgent) { + return; + } + + const widgetService = accessor.get(IChatWidgetService); + const widget = context?.widget ?? widgetService.lastFocusedWidget; + if (!widget) { + return; + } + + if (extractAgentAndCommand(widget.parsedInput).agentPart) { + widget.acceptInput(); + } else { + widget.lastSelectedAgent = secondaryAgent; + widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.name}`); + } + } +} + class SendToNewChatAction extends Action2 { constructor() { super({ @@ -100,19 +157,27 @@ export class CancelAction extends Action2 { id: MenuId.ChatExecute, when: CONTEXT_CHAT_REQUEST_IN_PROGRESS, group: 'navigation', + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.Escape, + win: { primary: KeyMod.Alt | KeyCode.Backspace }, } }); } run(accessor: ServicesAccessor, ...args: any[]) { - const context: IChatExecuteActionContext = args[0]; - if (!context.widget) { + const context: IChatExecuteActionContext | undefined = args[0]; + + const widgetService = accessor.get(IChatWidgetService); + const widget = context?.widget ?? widgetService.lastFocusedWidget; + if (!widget) { return; } const chatService = accessor.get(IChatService); - if (context.widget.viewModel) { - chatService.cancelCurrentRequestForSession(context.widget.viewModel.sessionId); + if (widget.viewModel) { + chatService.cancelCurrentRequestForSession(widget.viewModel.sessionId); } } } @@ -121,4 +186,5 @@ export function registerChatExecuteActions() { registerAction2(SubmitAction); registerAction2(CancelAction); registerAction2(SendToNewChatAction); + registerAction2(ChatSubmitSecondaryAgentAction); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatFileTreeActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatFileTreeActions.ts index 4b769210522..cb745f5f620 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatFileTreeActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatFileTreeActions.ts @@ -10,7 +10,7 @@ import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; export function registerChatFileTreeActions() { @@ -24,7 +24,7 @@ export function registerChatFileTreeActions() { weight: KeybindingWeight.WorkbenchContrib, when: CONTEXT_IN_CHAT_SESSION, }, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, }); @@ -45,7 +45,7 @@ export function registerChatFileTreeActions() { weight: KeybindingWeight.WorkbenchContrib, when: CONTEXT_IN_CHAT_SESSION, }, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts index a8a38ed3b6b..977cf2f7a14 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts @@ -6,7 +6,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { joinPath } from 'vs/base/common/resources'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { localize } from 'vs/nls'; +import { localize, localize2 } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; @@ -14,7 +14,7 @@ import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatAct import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; -import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { isExportableSessionData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -28,11 +28,8 @@ export function registerChatExportActions() { super({ id: 'workbench.action.chat.export', category: CHAT_CATEGORY, - title: { - value: localize('chat.export.label', "Export Session") + '...', - original: 'Export Session...' - }, - precondition: CONTEXT_PROVIDER_EXISTS, + title: localize2('chat.export.label', "Export Chat..."), + precondition: CONTEXT_CHAT_ENABLED, f1: true, }); } @@ -71,12 +68,9 @@ export function registerChatExportActions() { constructor() { super({ id: 'workbench.action.chat.import', - title: { - value: localize('chat.import.label', "Import Session") + '...', - original: 'Import Session...' - }, + title: localize2('chat.import.label', "Import Chat..."), category: CHAT_CATEGORY, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, }); } @@ -102,7 +96,8 @@ export function registerChatExportActions() { throw new Error('Invalid chat session data'); } - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { data }, pinned: true } }); + const options: IChatEditorOptions = { target: { data }, pinned: true }; + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }); } catch (err) { throw err; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts index 20e2bc80bb8..0f960b81e71 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -3,107 +3,68 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from 'vs/nls'; -import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { localize2 } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CHAT_CATEGORY, isChatViewTitleActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CHAT_VIEW_ID, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; enum MoveToNewLocation { Editor = 'Editor', Window = 'Window' } -const getMoveToChatActionDescriptorForViewTitle = (viewId: string, providerId: string, moveTo: MoveToNewLocation): Readonly & { viewId: string } => ({ - id: `workbench.action.chat.${providerId}.openIn${moveTo}`, - title: { - value: moveTo === MoveToNewLocation.Editor ? localize('chat.openInEditor.label', "Open Chat in Editor") : localize('chat.openInNewWindow.label', "Open Chat in New Window"), - original: moveTo === MoveToNewLocation.Editor ? 'Open Chat in Editor' : 'Open Chat in New Window', - }, - category: CHAT_CATEGORY, - precondition: CONTEXT_PROVIDER_EXISTS, - f1: false, - viewId, - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', viewId), - order: 0 - }, -}); - -export function getMoveToEditorAction(viewId: string, providerId: string) { - return getMoveToAction(viewId, providerId, MoveToNewLocation.Editor); -} - -export function getMoveToNewWindowAction(viewId: string, providerId: string) { - return getMoveToAction(viewId, providerId, MoveToNewLocation.Window); -} - -export function getMoveToAction(viewId: string, providerId: string, moveTo: MoveToNewLocation) { - return class MoveToAction extends ViewAction { - constructor() { - super(getMoveToChatActionDescriptorForViewTitle(viewId, providerId, moveTo)); - } - - async runInView(accessor: ServicesAccessor, view: ChatViewPane) { - const viewModel = view.widget.viewModel; - if (!viewModel) { - return; - } - - const editorService = accessor.get(IEditorService); - const sessionId = viewModel.sessionId; - const viewState = view.widget.getViewState(); - view.clear(); - - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId }, pinned: true, viewState: viewState } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); - } - }; -} - export function registerMoveActions() { registerAction2(class GlobalMoveToEditorAction extends Action2 { constructor() { super({ id: `workbench.action.chat.openInEditor`, - title: localize2('interactiveSession.openInEditor.label', "Open Chat in Editor"), + title: localize2('chat.openInEditor.label', "Open Chat in Editor"), category: CHAT_CATEGORY, - precondition: CONTEXT_PROVIDER_EXISTS, - f1: true + precondition: CONTEXT_CHAT_ENABLED, + f1: true, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), + order: 0 + }, }); } async run(accessor: ServicesAccessor, ...args: any[]) { - executeMoveToAction(accessor, MoveToNewLocation.Editor); + const context = args[0]; + executeMoveToAction(accessor, MoveToNewLocation.Editor, isChatViewTitleActionContext(context) ? context.chatView : undefined); } }); registerAction2(class GlobalMoveToNewWindowAction extends Action2 { - constructor() { super({ id: `workbench.action.chat.openInNewWindow`, - title: localize2('interactiveSession.openInNewWindow.label', "Open Chat in New Window"), + title: localize2('chat.openInNewWindow.label', "Open Chat in New Window"), category: CHAT_CATEGORY, - precondition: CONTEXT_PROVIDER_EXISTS, - f1: true + precondition: CONTEXT_CHAT_ENABLED, + f1: true, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), + order: 0 + }, }); } async run(accessor: ServicesAccessor, ...args: any[]) { - executeMoveToAction(accessor, MoveToNewLocation.Window); + const context = args[0]; + executeMoveToAction(accessor, MoveToNewLocation.Window, isChatViewTitleActionContext(context) ? context.chatView : undefined); } }); @@ -113,7 +74,7 @@ export function registerMoveActions() { id: `workbench.action.chat.openInSidebar`, title: localize2('interactiveSession.openInSidebar.label', "Open Chat in Side Bar"), category: CHAT_CATEGORY, - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, f1: true, menu: [{ id: MenuId.EditorTitle, @@ -129,17 +90,13 @@ export function registerMoveActions() { }); } -async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNewLocation) { +async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNewLocation, chatView?: ChatViewPane) { const widgetService = accessor.get(IChatWidgetService); - const viewService = accessor.get(IViewsService); - const chatService = accessor.get(IChatService); const editorService = accessor.get(IEditorService); - const widget = widgetService.lastFocusedWidget; + const widget = chatView?.widget ?? widgetService.lastFocusedWidget; if (!widget || !('viewId' in widget.viewContext)) { - const providerId = chatService.getProviderInfos()[0].id; - - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { providerId }, pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); return; } @@ -149,29 +106,27 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew } const sessionId = viewModel.sessionId; - const view = await viewService.openView(widget.viewContext.viewId) as ChatViewPane; - const viewState = view.widget.getViewState(); - view.clear(); + const viewState = widget.getViewState(); + widget.clear(); - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId }, pinned: true, viewState: viewState } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); + const options: IChatEditorOptions = { target: { sessionId }, pinned: true, viewState: viewState }; + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); } async function moveToSidebar(accessor: ServicesAccessor): Promise { const viewsService = accessor.get(IViewsService); const editorService = accessor.get(IEditorService); - const chatContribService = accessor.get(IChatContributionService); const editorGroupService = accessor.get(IEditorGroupsService); const chatEditorInput = editorService.activeEditor; - if (chatEditorInput instanceof ChatEditorInput && chatEditorInput.sessionId && chatEditorInput.providerId) { + let view: ChatViewPane; + if (chatEditorInput instanceof ChatEditorInput && chatEditorInput.sessionId) { await editorService.closeEditor({ editor: chatEditorInput, groupId: editorGroupService.activeGroup.id }); - const viewId = chatContribService.getViewIdForProvider(chatEditorInput.providerId); - const view = await viewsService.openView(viewId) as ChatViewPane; + view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; view.loadSession(chatEditorInput.sessionId); } else { - const chatService = accessor.get(IChatService); - const providerId = chatService.getProviderInfos()[0].id; - const viewId = chatContribService.getViewIdForProvider(providerId); - await viewsService.openView(viewId); + view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; } + + view.focus(); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts index d1863ef7d6c..f914b8902ee 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts @@ -12,13 +12,14 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IQuickChatService, IQuickChatOpenOptions } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IQuickChatOpenOptions, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; export const ASK_QUICK_QUESTION_ACTION_ID = 'workbench.action.quickchat.toggle'; export function registerQuickChatActions() { registerAction2(QuickChatGlobalAction); + registerAction2(AskQuickChatAction); registerAction2(class OpenInChatViewAction extends Action2 { constructor() { @@ -94,6 +95,7 @@ export function registerQuickChatActions() { controller.focus(); } }); + } class QuickChatGlobalAction extends Action2 { @@ -101,7 +103,7 @@ class QuickChatGlobalAction extends Action2 { super({ id: ASK_QUICK_QUESTION_ACTION_ID, title: localize2('quickChat', 'Quick Chat'), - precondition: CONTEXT_PROVIDER_EXISTS, + precondition: CONTEXT_CHAT_ENABLED, icon: Codicon.commentDiscussion, f1: false, category: CHAT_CATEGORY, @@ -153,35 +155,25 @@ class QuickChatGlobalAction extends Action2 { if (options?.query) { options.selection = new Selection(1, options.query.length + 1, 1, options.query.length + 1); } - quickChatService.toggle(undefined, options); + quickChatService.toggle(options); } } -/** - * Returns a provider specific action that will open the quick chat for that provider. - * This is used to include the provider label in the action title so it shows up in - * the command palette. - * @param id The id of the provider - * @param label The label of the provider - * @returns An action that will open the quick chat for this provider - */ -export function getQuickChatActionForProvider(id: string, label: string) { - return class AskQuickChatAction extends Action2 { - constructor() { - super({ - id: `workbench.action.openQuickChat.${id}`, - category: CHAT_CATEGORY, - title: localize2('interactiveSession.open', "Open Quick Chat ({0})", label), - f1: true - }); - } +class AskQuickChatAction extends Action2 { + constructor() { + super({ + id: `workbench.action.openQuickChat`, + category: CHAT_CATEGORY, + title: localize2('interactiveSession.open', "Open Quick Chat"), + f1: true + }); + } - override run(accessor: ServicesAccessor, query?: string): void { - const quickChatService = accessor.get(IQuickChatService); - quickChatService.toggle(id, query ? { - query, - selection: new Selection(1, query.length + 1, 1, query.length + 1) - } : undefined); - } - }; + override run(accessor: ServicesAccessor, query?: string): void { + const quickChatService = accessor.get(IQuickChatService); + quickChatService.toggle(query ? { + query, + selection: new Selection(1, query.length + 1, 1, query.length + 1) + } : undefined); + } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 8204a71dea1..c8bc363bd34 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -15,8 +15,8 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE, CONTEXT_VOTE_UP_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -37,7 +37,7 @@ export function registerChatTitleActions() { id: MenuId.ChatMessageTitle, group: 'navigation', order: 1, - when: CONTEXT_RESPONSE + when: ContextKeyExpr.and(CONTEXT_RESPONSE, CONTEXT_VOTE_UP_ENABLED) } }); } @@ -50,17 +50,17 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ - providerId: item.providerId, agentId: item.agent?.id, + command: item.slashCommand?.name, sessionId: item.sessionId, requestId: item.requestId, result: item.result, action: { kind: 'vote', - direction: InteractiveSessionVoteDirection.Up, + direction: ChatAgentVoteDirection.Up, } }); - item.setVote(InteractiveSessionVoteDirection.Up); + item.setVote(ChatAgentVoteDirection.Up); } }); @@ -90,17 +90,17 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ - providerId: item.providerId, agentId: item.agent?.id, + command: item.slashCommand?.name, sessionId: item.sessionId, requestId: item.requestId, result: item.result, action: { kind: 'vote', - direction: InteractiveSessionVoteDirection.Down, + direction: ChatAgentVoteDirection.Down, } }); - item.setVote(InteractiveSessionVoteDirection.Down); + item.setVote(ChatAgentVoteDirection.Down); } }); @@ -129,8 +129,8 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ - providerId: item.providerId, agentId: item.agent?.id, + command: item.slashCommand?.name, sessionId: item.sessionId, requestId: item.requestId, result: item.result, @@ -177,7 +177,7 @@ export function registerChatTitleActions() { return; } - const value = item.response.asString(); + const value = item.response.toString(); const splitContents = splitMarkdownAndCodeBlocks(value); const focusRange = notebookEditor.getFocus(); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 85c687b0cff..d1c81e35664 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; +import { MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh } from 'vs/base/common/platform'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as nls from 'vs/nls'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -18,47 +18,49 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { ChatAccessibilityHelp } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; +import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions'; -import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; -import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; +import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; -import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover'; +import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { LanguageModelToolsService, ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { ILanguageModelsService, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; -import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { ILanguageModelStatsService, LanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; +import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; +import { registerChatContextActions } from 'vs/workbench/contrib/chat/browser/actions/chatContextActions'; +import { registerChatDeveloperActions } from 'vs/workbench/contrib/chat/browser/actions/chatDeveloperActions'; +import { LanguageModelToolsExtensionPointHandler } from 'vs/workbench/contrib/chat/common/tools/languageModelToolsContribution'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -96,12 +98,31 @@ configurationRegistry.registerConfiguration({ 'chat.experimental.implicitContext': { type: 'boolean', description: nls.localize('chat.experimental.implicitContext', "Controls whether a checkbox is shown to allow the user to determine which implicit context is included with a chat participant's prompt."), + deprecated: true, + default: false + }, + 'chat.experimental.variables.editor': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.editor', "Enables variables for editor chat."), + default: true + }, + 'chat.experimental.variables.notebook': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.notebook', "Enables variables for notebook chat."), + default: false + }, + 'chat.experimental.variables.terminal': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.terminal', "Enables variables for terminal chat."), + default: false + }, + 'chat.experimental.detectParticipant.enabled': { + type: 'boolean', + description: nls.localize('chat.experimental.detectParticipant.enabled', "Enables chat participant autodetection for panel chat."), default: false }, } }); - - Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( ChatEditor, @@ -143,89 +164,8 @@ class ChatResolverContribution extends Disposable { } } -class ChatAccessibleViewContribution extends Disposable { - static ID: 'chatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'panelChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const widgetService = accessor.get(IChatWidgetService); - const codeEditorService = accessor.get(ICodeEditorService); - return renderAccessibleView(accessibleViewService, widgetService, codeEditorService, true); - function renderAccessibleView(accessibleViewService: IAccessibleViewService, widgetService: IChatWidgetService, codeEditorService: ICodeEditorService, initialRender?: boolean): boolean { - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return false; - } - const chatInputFocused = initialRender && !!codeEditorService.getFocusedCodeEditor(); - if (initialRender && chatInputFocused) { - widget.focusLastMessage(); - } - - if (!widget) { - return false; - } - - const verifiedWidget: IChatWidget = widget; - const focusedItem = verifiedWidget.getFocus(); - - if (!focusedItem) { - return false; - } - - widget.focus(focusedItem); - const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; - let responseContent = isResponseVM(focusedItem) ? focusedItem.response.asString() : undefined; - if (isWelcome) { - const welcomeReplyContents = []; - for (const content of focusedItem.content) { - if (Array.isArray(content)) { - welcomeReplyContents.push(...content.map(m => m.message)); - } else { - welcomeReplyContents.push((content as IMarkdownString).value); - } - } - responseContent = welcomeReplyContents.join('\n'); - } - if (!responseContent && 'errorDetails' in focusedItem && focusedItem.errorDetails) { - responseContent = focusedItem.errorDetails.message; - } - if (!responseContent) { - return false; - } - const responses = verifiedWidget.viewModel?.getItems().filter(i => isResponseVM(i)); - const length = responses?.length; - const responseIndex = responses?.findIndex(i => i === focusedItem); - - accessibleViewService.show({ - id: AccessibleViewProviderId.Chat, - verbositySettingKey: AccessibilityVerbositySettingId.Chat, - provideContent(): string { return responseContent!; }, - onClose() { - verifiedWidget.reveal(focusedItem); - if (chatInputFocused) { - verifiedWidget.focusInput(); - } else { - verifiedWidget.focus(focusedItem); - } - }, - next() { - verifiedWidget.moveFocus(focusedItem, 'next'); - alertFocusChange(responseIndex, length, 'next'); - renderAccessibleView(accessibleViewService, widgetService, codeEditorService); - }, - previous() { - verifiedWidget.moveFocus(focusedItem, 'previous'); - alertFocusChange(responseIndex, length, 'previous'); - renderAccessibleView(accessibleViewService, widgetService, codeEditorService); - }, - options: { type: AccessibleViewType.View } - }); - return true; - } - }, CONTEXT_IN_CHAT_SESSION)); - } -} +AccessibleViewRegistry.register(new ChatResponseAccessibleView()); +AccessibleViewRegistry.register(new ChatAccessibilityHelp()); class ChatSlashStaticSlashCommandsContribution extends Disposable { @@ -234,13 +174,15 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { @ICommandService commandService: ICommandService, @IChatAgentService chatAgentService: IChatAgentService, @IChatVariablesService chatVariablesService: IChatVariablesService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); this._store.add(slashCommandService.registerSlashCommand({ command: 'clear', detail: nls.localize('clear', "Start a new chat"), sortText: 'z2_clear', - executeImmediately: true + executeImmediately: true, + locations: [ChatAgentLocation.Panel] }, async () => { commandService.executeCommand(ACTION_ID_NEW_CHAT); })); @@ -248,7 +190,8 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { command: 'help', detail: '', sortText: 'z1_help', - executeImmediately: true + executeImmediately: true, + locations: [ChatAgentLocation.Panel] }, async (prompt, progress) => { const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); const agents = chatAgentService.getAgents(); @@ -258,23 +201,22 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { if (isMarkdownString(defaultAgent.metadata.helpTextPrefix)) { progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'markdownContent' }); } else { - progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'content' }); + progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPrefix), kind: 'markdownContent' }); } - progress.report({ content: '\n\n', kind: 'content' }); + progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); } // Report agent list const agentText = (await Promise.all(agents .filter(a => a.id !== defaultAgent?.id) + .filter(a => a.locations.includes(ChatAgentLocation.Panel)) .map(async a => { - const agentWithLeader = `${chatAgentLeader}${a.name}`; - const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` }; - const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); - const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.description}`; + const description = a.description ? `- ${a.description}` : ''; + const agentMarkdown = instantiationService.invokeFunction(accessor => agentToMarkdown(a, true, accessor)); + const agentLine = `- ${agentMarkdown} ${description}`; const commandText = a.slashCommands.map(c => { - const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; - const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); - return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${c.description}`; + const description = c.description ? `- ${c.description}` : ''; + return `\t* ${agentSlashCommandToMarkdown(a, c)} ${description}`; }).join('\n'); return (agentLine + '\n' + commandText).trim(); @@ -283,26 +225,30 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { // Report variables if (defaultAgent?.metadata.helpTextVariablesPrefix) { - progress.report({ content: '\n\n', kind: 'content' }); + progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); if (isMarkdownString(defaultAgent.metadata.helpTextVariablesPrefix)) { progress.report({ content: defaultAgent.metadata.helpTextVariablesPrefix, kind: 'markdownContent' }); } else { - progress.report({ content: defaultAgent.metadata.helpTextVariablesPrefix, kind: 'content' }); + progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextVariablesPrefix), kind: 'markdownContent' }); } - const variableText = Array.from(chatVariablesService.getVariables()) + const variables = [ + ...chatVariablesService.getVariables(ChatAgentLocation.Panel), + { name: 'file', description: nls.localize('file', "Choose a file in the workspace") } + ]; + const variableText = variables .map(v => `* \`${chatVariableLeader}${v.name}\` - ${v.description}`) .join('\n'); - progress.report({ content: '\n' + variableText, kind: 'content' }); + progress.report({ content: new MarkdownString('\n' + variableText), kind: 'markdownContent' }); } // Report help text ending if (defaultAgent?.metadata.helpTextPostfix) { - progress.report({ content: '\n\n', kind: 'content' }); + progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); if (isMarkdownString(defaultAgent.metadata.helpTextPostfix)) { progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'markdownContent' }); } else { - progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'content' }); + progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPostfix), kind: 'markdownContent' }); } } })); @@ -311,13 +257,16 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); -workbenchContributionsRegistry.registerWorkbenchContribution(ChatAccessibleViewContribution, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); +registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); registerChatActions(); registerChatCopyActions(); registerChatCodeBlockActions(); +registerChatCodeCompareBlockActions(); registerChatFileTreeActions(); registerChatTitleActions(); registerChatExecuteActions(); @@ -325,16 +274,20 @@ registerQuickChatActions(); registerChatExportActions(); registerMoveActions(); registerNewChatActions(); +registerChatContextActions(); +registerChatDeveloperActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); -registerSingleton(IChatContributionService, ChatContributionService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); registerSingleton(IQuickChatService, QuickChatService, InstantiationType.Delayed); registerSingleton(IChatAccessibilityService, ChatAccessibilityService, InstantiationType.Delayed); registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed); registerSingleton(ILanguageModelsService, LanguageModelsService, InstantiationType.Delayed); +registerSingleton(ILanguageModelStatsService, LanguageModelStatsService, InstantiationType.Delayed); registerSingleton(IChatSlashCommandService, ChatSlashCommandService, InstantiationType.Delayed); registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); +registerSingleton(IChatAgentNameService, ChatAgentNameService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); +registerSingleton(ILanguageModelToolsService, LanguageModelToolsService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 45ed72b2c0c..6d15c37c9d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -10,12 +10,17 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { IChatViewState, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export const IChatWidgetService = createDecorator('chatWidgetService'); @@ -28,25 +33,23 @@ export interface IChatWidgetService { */ readonly lastFocusedWidget: IChatWidget | undefined; - /** - * Returns whether a view was successfully revealed. - */ - revealViewForProvider(providerId: string): Promise; - getWidgetByInputUri(uri: URI): IChatWidget | undefined; - getWidgetBySessionId(sessionId: string): IChatWidget | undefined; } +export async function showChatView(viewsService: IViewsService): Promise { + return (await viewsService.openView(CHAT_VIEW_ID))?.widget; +} + export const IQuickChatService = createDecorator('quickChatService'); export interface IQuickChatService { readonly _serviceBrand: undefined; readonly onDidClose: Event; readonly enabled: boolean; readonly focused: boolean; - toggle(providerId?: string, options?: IQuickChatOpenOptions): void; + toggle(options?: IQuickChatOpenOptions): void; focus(): void; - open(providerId?: string, options?: IQuickChatOpenOptions): void; + open(options?: IQuickChatOpenOptions): void; close(): void; openInChatView(): void; } @@ -75,7 +78,8 @@ export interface IChatAccessibilityService { export interface IChatCodeBlockInfo { codeBlockIndex: number; - element: IChatResponseViewModel; + element: ChatTreeItem; + uri: URI | undefined; focus(): void; } @@ -87,15 +91,33 @@ export interface IChatFileTreeInfo { export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel; +export interface IChatListItemRendererOptions { + readonly renderStyle?: 'default' | 'compact' | 'minimal'; + readonly noHeader?: boolean; + readonly noPadding?: boolean; + readonly editableCodeBlock?: boolean; + readonly renderTextEditsAsSummary?: (uri: URI) => boolean; +} + export interface IChatWidgetViewOptions { renderInputOnTop?: boolean; - renderStyle?: 'default' | 'compact'; + renderFollowups?: boolean; + renderStyle?: 'default' | 'compact' | 'minimal'; supportsFileReferences?: boolean; filter?: (item: ChatTreeItem) => boolean; - editableCodeBlocks?: boolean; + rendererOptions?: IChatListItemRendererOptions; menus?: { + /** + * The menu that is inside the input editor, use for send, dictation + */ executeToolbar?: MenuId; + /** + * The menu that next to the input editor, use for close, config etc + */ inputSideToolbar?: MenuId; + /** + * The telemetry source for all commands of this widget + */ telemetrySource?: string; }; defaultElementHeight?: number; @@ -115,24 +137,29 @@ export type IChatWidgetViewContext = IChatViewViewContext | IChatResourceViewCon export interface IChatWidget { readonly onDidChangeViewModel: Event; readonly onDidAcceptInput: Event; + readonly onDidHide: Event; readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; + readonly onDidChangeAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; + readonly onDidChangeParsedInput: Event; + readonly onDidChangeContext: Event<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>; readonly location: ChatAgentLocation; readonly viewContext: IChatWidgetViewContext; readonly viewModel: IChatViewModel | undefined; readonly inputEditor: ICodeEditor; - readonly providerId: string; readonly supportsFileReferences: boolean; readonly parsedInput: IParsedChatRequest; lastSelectedAgent: IChatAgentData | undefined; + readonly scopedContextKeyService: IContextKeyService; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; focus(item: ChatTreeItem): void; - moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void; + getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined; getFocus(): ChatTreeItem | undefined; setInput(query?: string): void; getInput(): string; - acceptInput(query?: string): void; + logInputHistory(): void; + acceptInput(query?: string): Promise; acceptInputWithPrefix(prefix: string): void; setInputPlaceholder(placeholder: string): void; resetInputPlaceholder(): void; @@ -143,11 +170,9 @@ export interface IChatWidget { getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[]; getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[]; getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; + setContext(overwrite: boolean, ...context: IChatRequestVariableEntry[]): void; clear(): void; -} - -export interface IChatViewPane { - clear(): void; + getViewState(): IChatViewState; } @@ -163,3 +188,5 @@ export interface IChatCodeBlockContextProviderService { } export const GeneratingPhrase = localize('generating', "Generating"); + +export const CHAT_VIEW_ID = `workbench.panel.chat.view.${CHAT_PROVIDER_ID}`; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts index 933a8940869..ecfaa1653f1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -8,7 +8,7 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { marked } from 'vs/base/common/marked/marked'; import { localize } from 'vs/nls'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { isRequestVM, isResponseVM, isWelcomeVM, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -62,16 +62,16 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider token.type === 'code')?.length ?? 0; + const codeBlockCount = marked.lexer(element.response.toString()).filter(token => token.type === 'code')?.length ?? 0; switch (codeBlockCount) { case 0: - label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.asString()); + label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.toString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.toString()); break; case 1: - label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.asString()); + label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.toString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.toString()); break; default: - label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.asString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.asString()); + label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.toString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.toString()); break; } return label; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index 73a0ebd76a9..17b9a5e9945 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -4,62 +4,51 @@ *--------------------------------------------------------------------------------------------*/ import { status } from 'vs/base/browser/ui/aria/aria'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { AccessibilityVoiceSettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService { declare readonly _serviceBrand: undefined; - private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); + private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); private _requestId: number = 0; - constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IInstantiationService private readonly _instantiationService: IInstantiationService) { + constructor( + @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService + ) { super(); } acceptRequest(): number { this._requestId++; this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); - this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilitySignalScheduler)); + this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilityProgressSignalScheduler, CHAT_RESPONSE_PENDING_ALLOWANCE_MS, undefined)); return this._requestId; } acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; - const responseContent = typeof response === 'string' ? response : response?.response.asString(); + const responseContent = typeof response === 'string' ? response : response?.response.toString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); - if (!response) { + if (!response || !responseContent) { return; } const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; - status(responseContent + errorDetails); - } -} - -const CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS = 5000; -const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; -/** - * Schedules an audio cue to play when a chat response is pending for too long. - */ -class AccessibilitySignalScheduler extends Disposable { - private _scheduler: RunOnceScheduler; - private _signalLoop: IDisposable | undefined; - constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { - super(); - this._scheduler = new RunOnceScheduler(() => { - this._signalLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); - }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS); - this._scheduler.schedule(); - } - override dispose(): void { - super.dispose(); - this._signalLoop?.dispose(); - this._scheduler.cancel(); - this._scheduler.dispose(); + const plainTextResponse = renderStringAsPlaintext(new MarkdownString(responseContent)); + if (this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') { + status(plainTextResponse + errorDetails); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts new file mode 100644 index 00000000000..78f34762cf0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { verifiedPublisherIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; + +export class ChatAgentHover extends Disposable { + public readonly domNode: HTMLElement; + + private readonly icon: HTMLElement; + private readonly name: HTMLElement; + private readonly extensionName: HTMLElement; + private readonly publisherName: HTMLElement; + private readonly description: HTMLElement; + + private readonly _onDidChangeContents = this._register(new Emitter()); + public readonly onDidChangeContents: Event = this._onDidChangeContents.event; + + constructor( + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, + ) { + super(); + + const hoverElement = dom.h( + '.chat-agent-hover@root', + [ + dom.h('.chat-agent-hover-header', [ + dom.h('.chat-agent-hover-icon@icon'), + dom.h('.chat-agent-hover-details', [ + dom.h('.chat-agent-hover-name@name'), + dom.h('.chat-agent-hover-extension', [ + dom.h('.chat-agent-hover-extension-name@extensionName'), + dom.h('.chat-agent-hover-separator@separator'), + dom.h('.chat-agent-hover-publisher@publisher'), + ]), + ]), + ]), + dom.h('.chat-agent-hover-warning@warning'), + dom.h('span.chat-agent-hover-description@description'), + ]); + this.domNode = hoverElement.root; + + this.icon = hoverElement.icon; + this.name = hoverElement.name; + this.extensionName = hoverElement.extensionName; + this.description = hoverElement.description; + + hoverElement.separator.textContent = '|'; + + const verifiedBadge = dom.$('span.extension-verified-publisher', undefined, renderIcon(verifiedPublisherIcon)); + + this.publisherName = dom.$('span.chat-agent-hover-publisher-name'); + dom.append( + hoverElement.publisher, + verifiedBadge, + this.publisherName); + + hoverElement.warning.appendChild(renderIcon(Codicon.warning)); + hoverElement.warning.appendChild(dom.$('span', undefined, localize('reservedName', "This chat extension is using a reserved name."))); + } + + setAgent(id: string): void { + const agent = this.chatAgentService.getAgent(id)!; + if (agent.metadata.icon instanceof URI) { + const avatarIcon = dom.$('img.icon'); + avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.icon).toString(true); + this.icon.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); + } else if (agent.metadata.themeIcon) { + const avatarIcon = dom.$(ThemeIcon.asCSSSelector(agent.metadata.themeIcon)); + this.icon.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); + } + + this.domNode.classList.toggle('noExtensionName', !!agent.isDynamic); + + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); + this.name.textContent = isAllowed ? `@${agent.name}` : getFullyQualifiedId(agent); + this.extensionName.textContent = agent.extensionDisplayName; + this.publisherName.textContent = agent.publisherDisplayName ?? agent.extensionPublisherId; + + let description = agent.description ?? ''; + if (description) { + if (!description.match(/[\.\?\!] *$/)) { + description += '.'; + } + } + + this.description.textContent = description; + this.domNode.classList.toggle('allowedName', isAllowed); + + this.domNode.classList.toggle('verifiedPublisher', false); + if (!agent.isDynamic) { + const cancel = this._register(new CancellationTokenSource()); + this.extensionService.getExtensions([{ id: agent.extensionId.value }], cancel.token).then(extensions => { + cancel.dispose(); + const extension = extensions[0]; + if (extension?.publisherDomain?.verified) { + this.domNode.classList.toggle('verifiedPublisher', true); + this._onDidChangeContents.fire(); + } + }); + } + } +} + +export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IManagedHoverOptions { + return { + actions: [ + { + commandId: showExtensionsWithIdsCommandId, + label: localize('viewExtensionLabel', "View Extension"), + run: () => { + const agent = getAgent(); + if (agent) { + commandService.executeCommand(showExtensionsWithIdsCommandId, [agent.extensionId.value]); + } + }, + } + ] + }; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts new file mode 100644 index 00000000000..407e5d75582 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; +import { Emitter } from 'vs/base/common/event'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; +import { URI } from 'vs/base/common/uri'; +import { FileKind } from 'vs/platform/files/common/files'; +import { Range } from 'vs/editor/common/core/range'; +import { basename, dirname } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; +import { ChatResponseReferencePartStatusKind, IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +export class ChatAttachmentsContentPart extends Disposable { + private readonly attachedContextDisposables = this._register(new DisposableStore()); + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + + constructor( + private readonly variables: IChatRequestVariableEntry[], + private readonly contentReferences: ReadonlyArray = [], + public readonly domNode: HTMLElement = dom.$('.chat-attached-context'), + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super(); + + this.initAttachedContext(domNode); + } + + private initAttachedContext(container: HTMLElement) { + dom.clearNode(container); + this.attachedContextDisposables.clear(); + dom.setVisibility(Boolean(this.variables.length), this.domNode); + + this.variables.forEach((attachment) => { + const widget = dom.append(container, dom.$('.chat-attached-context-attachment.show-file-icons')); + const label = this._contextResourceLabels.create(widget, { supportIcons: true }); + const file = 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 correspondingContentReference = this.contentReferences.find((ref) => 'variableName' in ref.reference && ref.reference.variableName === attachment.name); + const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; + const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; + + if (file) { + const fileBasename = basename(file.path); + const fileDirname = dirname(file.path); + const friendlyName = `${fileBasename} ${fileDirname}`; + let ariaLabel; + if (isAttachmentOmitted) { + ariaLabel = range ? localize('chat.omittedFileAttachmentWithRange', "Omitted: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.omittedFileAttachment', "Omitted: {0}.", friendlyName); + } else if (isAttachmentPartialOrOmitted) { + ariaLabel = range ? localize('chat.partialFileAttachmentWithRange', "Partially attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.partialFileAttachment', "Partially attached: {0}.", friendlyName); + } else { + ariaLabel = range ? localize('chat.fileAttachmentWithRange3', "Attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment3', "Attached: {0}.", friendlyName); + } + + label.setFile(file, { + fileKind: FileKind.FILE, + hidePath: true, + range, + title: correspondingContentReference?.options?.status?.description + }); + widget.ariaLabel = ariaLabel; + widget.tabIndex = 0; + widget.style.cursor = 'pointer'; + + this.attachedContextDisposables.add(dom.addDisposableListener(widget, dom.EventType.CLICK, async (e: MouseEvent) => { + dom.EventHelper.stop(e, true); + if (file) { + this.openerService.open( + file, + { + fromUserGesture: true, + editorOptions: { + selection: range + } as any + }); + } + })); + } else { + const attachmentLabel = attachment.fullName ?? attachment.name; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; + label.setLabel(withIcon, correspondingContentReference?.options?.status?.description); + + widget.ariaLabel = localize('chat.attachment3', "Attached context: {0}.", attachment.name); + widget.tabIndex = 0; + } + + if (isAttachmentPartialOrOmitted) { + widget.classList.add('warning'); + } + const description = correspondingContentReference?.options?.status?.description; + if (isAttachmentPartialOrOmitted) { + widget.ariaLabel = `${widget.ariaLabel}${description ? ` ${description}` : ''}`; + for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { + const element = label.element.querySelector(selector); + if (element) { + element.classList.add('warning'); + } + } + } + }); + } +} + diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart.ts new file mode 100644 index 00000000000..fedc276bdee --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { getCodeCitationsMessage } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatCodeCitations, IChatRendererContent } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +type ChatCodeCitationOpenedClassification = { + owner: 'roblourens'; + comment: 'Indicates when a user opens chat code citations'; +}; + +export class ChatCodeCitationContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + citations: IChatCodeCitations, + context: IChatContentPartRenderContext, + @IEditorService private readonly editorService: IEditorService, + @ITelemetryService private readonly telemetryService: ITelemetryService + ) { + super(); + + const label = getCodeCitationsMessage(citations.citations); + const elements = dom.h('.chat-code-citation-message@root', [ + dom.h('span.chat-code-citation-label@label'), + dom.h('.chat-code-citation-button-container@button'), + ]); + elements.label.textContent = label + ' - '; + const button = this._register(new Button(elements.button, { + buttonBackground: undefined, + buttonBorder: undefined, + buttonForeground: undefined, + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined + })); + button.label = localize('viewMatches', "View matches"); + this._register(button.onDidClick(() => { + const citationText = `# Code Citations\n\n` + citations.citations.map(c => `## License: ${c.license}\n${c.value.toString()}\n\n\`\`\`\n${c.snippet}\n\`\`\`\n\n`).join('\n'); + this.editorService.openEditor({ resource: undefined, contents: citationText, languageId: 'markdown' }); + this.telemetryService.publicLog2<{}, ChatCodeCitationOpenedClassification>('openedChatCodeCitations'); + })); + this.domNode = elements.root; + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'codeCitations'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts new file mode 100644 index 00000000000..d13bbdff23b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; + +export class ResourcePool extends Disposable { + private readonly pool: T[] = []; + + private _inUse = new Set; + public get inUse(): ReadonlySet { + return this._inUse; + } + + constructor( + private readonly _itemFactory: () => T, + ) { + super(); + } + + get(): T { + if (this.pool.length > 0) { + const item = this.pool.pop()!; + this._inUse.add(item); + return item; + } + + const item = this._register(this._itemFactory()); + this._inUse.add(item); + return item; + } + + release(item: T): void { + this._inUse.delete(item); + this.pool.push(item); + } +} + +export interface IDisposableReference extends IDisposable { + object: T; + isStale: () => boolean; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts new file mode 100644 index 00000000000..3893117fd20 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatCommandButton } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +const $ = dom.$; + +export class ChatCommandButtonContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + commandButton: IChatCommandButton, + context: IChatContentPartRenderContext, + @ICommandService private readonly commandService: ICommandService + ) { + super(); + + this.domNode = $('.chat-command-button'); + const enabled = !isResponseVM(context.element) || !context.element.isStale; + const tooltip = enabled ? + commandButton.command.tooltip : + localize('commandButtonDisabled', "Button not available in restored chat"); + const button = this._register(new Button(this.domNode, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); + button.label = commandButton.command.title; + button.enabled = enabled; + + // TODO still need telemetry for command buttons + this._register(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'command'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts new file mode 100644 index 00000000000..a3957178e57 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.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 { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatConfirmation, IChatSendRequestOptions, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatConfirmationContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + confirmation: IChatConfirmation, + context: IChatContentPartRenderContext, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + + const element = context.element; + const buttons = confirmation.buttons + ? confirmation.buttons.map(button => ({ + label: button, + data: confirmation.data + })) + : [ + { label: localize('accept', "Accept"), data: confirmation.data }, + { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, + ]; + const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, buttons)); + confirmationWidget.setShowButtons(!confirmation.isUsed); + + this._register(confirmationWidget.onDidClick(async e => { + if (isResponseVM(element)) { + const prompt = `${e.label}: "${confirmation.title}"`; + const data: IChatSendRequestOptions = e.isSecondary ? + { rejectedConfirmationData: [e.data] } : + { acceptedConfirmationData: [e.data] }; + data.agentId = element.agent?.id; + data.slashCommand = element.slashCommand?.name; + data.confirmation = e.label; + if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { + confirmation.isUsed = true; + confirmationWidget.setShowButtons(false); + this._onDidChangeHeight.fire(); + } + } + })); + + this.domNode = confirmationWidget.domNode; + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'confirmation'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts new file mode 100644 index 00000000000..ea5cf39c113 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.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 * as dom from 'vs/base/browser/dom'; +import 'vs/css!./media/chatConfirmationWidget'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Emitter, Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; + +export interface IChatConfirmationButton { + label: string; + isSecondary?: boolean; + data: any; +} + +export class ChatConfirmationWidget extends Disposable { + private _onDidClick = this._register(new Emitter()); + get onDidClick(): Event { return this._onDidClick.event; } + + private _domNode: HTMLElement; + get domNode(): HTMLElement { + return this._domNode; + } + + setShowButtons(showButton: boolean): void { + this.domNode.classList.toggle('hideButtons', !showButton); + } + + constructor( + title: string, + message: string, + buttons: IChatConfirmationButton[], + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + const elements = dom.h('.chat-confirmation-widget@root', [ + dom.h('.chat-confirmation-widget-title@title'), + dom.h('.chat-confirmation-widget-message@message'), + dom.h('.chat-confirmation-buttons-container@buttonsContainer'), + ]); + this._domNode = elements.root; + const renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + const renderedTitle = this._register(renderer.render(new MarkdownString(title))); + elements.title.appendChild(renderedTitle.element); + + const renderedMessage = this._register(renderer.render(new MarkdownString(message))); + elements.message.appendChild(renderedMessage.element); + + buttons.forEach(buttonData => { + const button = new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary }); + button.label = buttonData.label; + this._register(button.onDidClick(() => this._onDidClick.fire(buttonData))); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts new file mode 100644 index 00000000000..80e3c44a056 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/lifecycle'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatRendererContent } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export interface IChatContentPart extends IDisposable { + domNode: HTMLElement; + + /** + * Returns true if the other content is equivalent to what is already rendered in this content part. + * Returns false if a rerender is needed. + * followingContent is all the content that will be rendered after this content part (to support progress messages' behavior). + */ + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean; +} + +export interface IChatContentPartRenderContext { + element: ChatTreeItem; + index: number; + content: ReadonlyArray; + preceedingContentParts: ReadonlyArray; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts new file mode 100644 index 00000000000..29a2573374b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { Range } from 'vs/editor/common/core/range'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatCodeBlockInfo, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CodeBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { IMarkdownVulnerability } from 'vs/workbench/contrib/chat/common/annotations'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; + +const $ = dom.$; + +export class ChatMarkdownContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + private readonly allRefs: IDisposableReference[] = []; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + private readonly markdown: IMarkdownString, + context: IChatContentPartRenderContext, + private readonly editorPool: EditorPool, + fillInIncompleteTokens = false, + codeBlockStartIndex = 0, + renderer: MarkdownRenderer, + currentWidth: number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + rendererOptions: IChatListItemRendererOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @ITextModelService private readonly textModelService: ITextModelService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const element = context.element; + const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); + + // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering + const orderedDisposablesList: IDisposable[] = []; + let codeBlockIndex = codeBlockStartIndex; + const result = this._register(renderer.render(markdown, { + fillInIncompleteTokens, + codeBlockRendererSync: (languageId, text) => { + const index = codeBlockIndex++; + let textModel: Promise; + let range: Range | undefined; + let vulns: readonly IMarkdownVulnerability[] | undefined; + if (equalsIgnoreCase(languageId, localFileLanguageId)) { + try { + const parsedBody = parseLocalFileData(text); + range = parsedBody.range && Range.lift(parsedBody.range); + textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object); + } catch (e) { + return $('div'); + } + } else { + if (!isRequestVM(element) && !isResponseVM(element)) { + console.error('Trying to render code block in welcome', element.id, index); + return $('div'); + } + + const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; + const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); + vulns = modelEntry.vulns; + textModel = modelEntry.model; + } + + const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; + const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns }, text, currentWidth, rendererOptions.editableCodeBlock); + this.allRefs.push(ref); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); + + const info: IChatCodeBlockInfo = { + codeBlockIndex: index, + element, + focus() { + ref.object.focus(); + }, + uri: ref.object.uri + }; + this.codeblocks.push(info); + orderedDisposablesList.push(ref); + return ref.object.element; + }, + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + })); + + this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element)); + + orderedDisposablesList.reverse().forEach(d => this._register(d)); + this.domNode = result.element; + } + + private renderCodeBlock(data: ICodeBlockData, text: string, currentWidth: number, editableCodeBlock: boolean | undefined): IDisposableReference { + const ref = this.editorPool.get(); + const editorInfo = ref.object; + if (isResponseVM(data.element)) { + this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + } + + editorInfo.render(data, currentWidth, editableCodeBlock); + + return ref; + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + return other.kind === 'markdownContent' && other.content.value === this.markdown.value; + } + + layout(width: number): void { + this.allRefs.forEach(ref => ref.object.layout(width)); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class EditorPool extends Disposable { + + private readonly _pool: ResourcePool; + + public inUse(): Iterable { + return this._pool.inUse; + } + + constructor( + options: ChatEditorOptions, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); + })); + } + + get(): IDisposableReference { + const codeBlock = this._pool.get(); + let stale = false; + return { + object: codeBlock, + isStale: () => stale, + dispose: () => { + codeBlock.reset(); + stale = true; + this._pool.release(codeBlock); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts new file mode 100644 index 00000000000..6545fdfe9ad --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from 'vs/base/browser/dom'; +import { alert } from 'vs/base/browser/ui/aria/aria'; +import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressMessage, IChatTask } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatRendererContent, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatProgressContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly showSpinner: boolean; + + constructor( + progress: IChatProgressMessage | IChatTask, + renderer: MarkdownRenderer, + context: IChatContentPartRenderContext, + forceShowSpinner?: boolean, + forceShowMessage?: boolean + ) { + super(); + + const followingContent = context.content.slice(context.index + 1); + this.showSpinner = forceShowSpinner ?? shouldShowSpinner(followingContent, context.element); + const hideMessage = forceShowMessage !== true && followingContent.some(part => part.kind !== 'progressMessage'); + if (hideMessage) { + // Placeholder, don't show the progress message + this.domNode = $(''); + return; + } + + if (this.showSpinner) { + // TODO@roblourens is this the right place for this? + // this step is in progress, communicate it to SR users + alert(progress.content.value); + } + const codicon = this.showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin').id : Codicon.check.id; + const markdown = new MarkdownString(`$(${codicon}) ${progress.content.value}`, { + supportThemeIcons: true + }); + const result = this._register(renderer.render(markdown)); + result.element.classList.add('progress-step'); + + this.domNode = result.element; + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + // Needs rerender when spinner state changes + const showSpinner = shouldShowSpinner(followingContent, element); + return other.kind === 'progressMessage' && this.showSpinner === showSpinner; + } +} + +function shouldShowSpinner(followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return isResponseVM(element) && !element.isComplete && followingContent.length === 0; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts new file mode 100644 index 00000000000..99b28473495 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -0,0 +1,334 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { matchesSomeScheme, Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/path'; +import { basenameOrAuthority, isEqualAuthority } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { FileKind } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { ColorScheme } from 'vs/workbench/browser/web.api'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { ChatResponseReferencePartStatusKind, IChatContentReference, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatRendererContent, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; +import { SETTINGS_AUTHORITY } from 'vs/workbench/services/preferences/common/preferences'; + +const $ = dom.$; + +export interface IChatReferenceListItem extends IChatContentReference { + title?: string; +} + +export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; + +export class ChatCollapsibleListContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + private readonly data: ReadonlyArray, + labelOverride: string | undefined, + element: IChatResponseViewModel, + contentReferencesListPool: CollapsibleListPool, + @IOpenerService openerService: IOpenerService, + ) { + super(); + + const referencesLabel = labelOverride ?? (data.length > 1 ? + localize('usedReferencesPlural', "Used {0} references", data.length) : + localize('usedReferencesSingular', "Used {0} reference", 1)); + const iconElement = $('.chat-used-context-icon'); + const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; + iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); + const buttonElement = $('.chat-used-context-label', undefined); + + const collapseButton = this._register(new Button(buttonElement, { + buttonBackground: undefined, + buttonBorder: undefined, + buttonForeground: undefined, + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined + })); + this.domNode = $('.chat-used-context', undefined, buttonElement); + collapseButton.label = referencesLabel; + collapseButton.element.prepend(iconElement); + this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); + this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); + this._register(collapseButton.onDidClick(() => { + iconElement.classList.remove(...ThemeIcon.asClassNameArray(icon(element))); + element.usedReferencesExpanded = !element.usedReferencesExpanded; + iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); + this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); + this._onDidChangeHeight.fire(); + this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); + })); + + const ref = this._register(contentReferencesListPool.get()); + const list = ref.object; + this.domNode.appendChild(list.getHTMLElement().parentElement!); + + this._register(list.onDidOpen((e) => { + if (e.element && 'reference' in e.element) { + const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; + const uri = URI.isUri(uriOrLocation) ? uriOrLocation : + uriOrLocation?.uri; + if (uri) { + openerService.open( + uri, + { + fromUserGesture: true, + editorOptions: { + ...e.editorOptions, + ...{ + selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined + } + } + }); + } + } + })); + this._register(list.onContextMenu((e) => { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + })); + + const maxItemsShown = 6; + const itemsShown = Math.min(data.length, maxItemsShown); + const height = itemsShown * 22; + list.layout(height); + list.getHTMLElement().style.height = `${height}px`; + list.splice(0, list.length, data); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'references' && other.references.length === this.data.length || + other.kind === 'codeCitations' && other.citations.length === this.data.length; + } + + private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { + element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class CollapsibleListPool extends Disposable { + private _pool: ResourcePool>; + + public get inUse(): ReadonlySet> { + return this._pool.inUse; + } + + constructor( + private _onDidChangeVisibility: Event, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService private readonly themeService: IThemeService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => this.listFactory())); + } + + private listFactory(): WorkbenchList { + const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); + + const container = $('.chat-used-context-list'); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); + + const list = this.instantiationService.createInstance( + WorkbenchList, + 'ChatListRenderer', + container, + new CollapsibleListDelegate(), + [this.instantiationService.createInstance(CollapsibleListRenderer, resourceLabels)], + { + alwaysConsumeMouseWheel: false, + accessibilityProvider: { + getAriaLabel: (element: IChatCollapsibleListItem) => { + if (element.kind === 'warning') { + return element.content.value; + } + const reference = element.reference; + if ('variableName' in reference) { + return reference.variableName; + } else if (URI.isUri(reference)) { + return basename(reference.path); + } else { + return basename(reference.uri.path); + } + }, + + getWidgetAriaLabel: () => localize('chatCollapsibleList', "Collapsible Chat List") + }, + dnd: { + getDragURI: (element: IChatCollapsibleListItem) => { + if (element.kind === 'warning') { + return null; + } + const { reference } = element; + if ('variableName' in reference) { + return null; + } else if (URI.isUri(reference)) { + return reference.toString(); + } else { + return reference.uri.toString(); + } + }, + dispose: () => { }, + onDragOver: () => false, + drop: () => { }, + }, + }); + + return list; + } + + get(): IDisposableReference> { + const object = this._pool.get(); + let stale = false; + return { + object, + isStale: () => stale, + dispose: () => { + stale = true; + this._pool.release(object); + } + }; + } +} + +class CollapsibleListDelegate implements IListVirtualDelegate { + getHeight(element: IChatCollapsibleListItem): number { + return 22; + } + + getTemplateId(element: IChatCollapsibleListItem): string { + return CollapsibleListRenderer.TEMPLATE_ID; + } +} + +interface ICollapsibleListTemplate { + label: IResourceLabel; + templateDisposables: IDisposable; +} + +class CollapsibleListRenderer implements IListRenderer { + static TEMPLATE_ID = 'chatCollapsibleListRenderer'; + readonly templateId: string = CollapsibleListRenderer.TEMPLATE_ID; + + constructor( + private labels: ResourceLabels, + @IThemeService private readonly themeService: IThemeService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + @IProductService private readonly productService: IProductService, + ) { } + + renderTemplate(container: HTMLElement): ICollapsibleListTemplate { + const templateDisposables = new DisposableStore(); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true, supportIcons: true })); + return { templateDisposables, label }; + } + + + private getReferenceIcon(data: IChatContentReference): URI | ThemeIcon | undefined { + if (ThemeIcon.isThemeIcon(data.iconPath)) { + return data.iconPath; + } else { + return this.themeService.getColorTheme().type === ColorScheme.DARK && data.iconPath?.dark + ? data.iconPath?.dark + : data.iconPath?.light; + } + } + + renderElement(data: IChatCollapsibleListItem, index: number, templateData: ICollapsibleListTemplate, height: number | undefined): void { + if (data.kind === 'warning') { + templateData.label.setResource({ name: data.content.value }, { icon: Codicon.warning }); + return; + } + + const reference = data.reference; + const icon = this.getReferenceIcon(data); + templateData.label.element.style.display = 'flex'; + if ('variableName' in reference) { + if (reference.value) { + const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; + templateData.label.setResource( + { + resource: uri, + name: basenameOrAuthority(uri), + description: `#${reference.variableName}`, + range: 'range' in reference.value ? reference.value.range : undefined, + }, { icon, title: data.options?.status?.description ?? data.title }); + } else { + const variable = this.chatVariablesService.getVariable(reference.variableName); + // This is a hack to get chat attachment ThemeIcons to render for resource labels + const asThemeIcon = variable?.icon ? `$(${variable.icon.id}) ` : ''; + const asVariableName = `#${reference.variableName}`; // Fallback, shouldn't really happen + const label = `${asThemeIcon}${variable?.fullName ?? asVariableName}`; + templateData.label.setLabel(label, asVariableName, { title: data.options?.status?.description ?? variable?.description }); + } + } else { + const uri = 'uri' in reference ? reference.uri : reference; + if (uri.scheme === 'https' && isEqualAuthority(uri.authority, 'github.com') && uri.path.includes('/tree/')) { + // Parse a nicer label for GitHub URIs that point at a particular commit + file + const label = uri.path.split('/').slice(1, 3).join('/'); + const description = uri.path.split('/').slice(5).join('/'); + templateData.label.setResource({ resource: uri, name: label, description }, { icon: Codicon.github, title: data.title }); + } else if (uri.scheme === this.productService.urlProtocol && isEqualAuthority(uri.authority, SETTINGS_AUTHORITY)) { + // a nicer label for settings URIs + const settingId = uri.path.substring(1); + templateData.label.setResource({ resource: uri, name: settingId }, { icon: Codicon.settingsGear, title: localize('setting.hover', "Open setting '{0}'", settingId) }); + } else if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { + templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe, title: data.options?.status?.description ?? data.title ?? uri.toString() }); + } else { + templateData.label.setFile(uri, { + fileKind: FileKind.FILE, + // Should not have this live-updating data on a historical reference + fileDecorations: { badges: false, colors: false }, + range: 'range' in reference ? reference.range : undefined, + title: data.options?.status?.description ?? data.title + }); + } + } + + for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { + const element = templateData.label.element.querySelector(selector); + if (element) { + if (data.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted || data.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial) { + element.classList.add('warning'); + } else { + element.classList.remove('warning'); + } + } + } + } + + disposeTemplate(templateData: ICollapsibleListTemplate): void { + templateData.templateDisposables.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts new file mode 100644 index 00000000000..8f376c698e8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { ChatProgressContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart'; +import { ChatCollapsibleListContentPart, CollapsibleListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatTask } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatTaskContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + public readonly onDidChangeHeight: Event; + + constructor( + private readonly task: IChatTask, + contentReferencesListPool: CollapsibleListPool, + renderer: MarkdownRenderer, + context: IChatContentPartRenderContext, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + if (task.progress.length) { + const refsPart = this._register(instantiationService.createInstance(ChatCollapsibleListContentPart, task.progress, task.content.value, context.element as IChatResponseViewModel, contentReferencesListPool)); + this.domNode = dom.$('.chat-progress-task'); + this.domNode.appendChild(refsPart.domNode); + this.onDidChangeHeight = refsPart.onDidChangeHeight; + } else { + // #217645 + const isSettled = task.isSettled?.() ?? true; + const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, task, renderer, context, !isSettled, true)); + this.domNode = progressPart.domNode; + this.onDidChangeHeight = Event.None; + } + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + return other.kind === 'progressTask' + && other.progress.length === this.task.progress.length + && other.isSettled() === this.task.isSettled(); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts new file mode 100644 index 00000000000..61bbdb4f99d --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts @@ -0,0 +1,212 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { TextEdit } from 'vs/editor/common/languages'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CodeCompareBlockPart, ICodeCompareBlockData, ICodeCompareBlockDiffData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +const $ = dom.$; + +export class ChatTextEditContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + private readonly ref: IDisposableReference | undefined; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + chatTextEdit: IChatTextEditGroup, + context: IChatContentPartRenderContext, + rendererOptions: IChatListItemRendererOptions, + diffEditorPool: DiffEditorPool, + currentWidth: number, + @ITextModelService private readonly textModelService: ITextModelService, + @IModelService private readonly modelService: IModelService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + const element = context.element; + + // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen + if (rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) { + if (isResponseVM(element) && element.response.value.every(item => item.kind === 'textEditGroup')) { + this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete + ? '' + : element.isCanceled + ? localize('edits0', "Making changes was aborted.") + : localize('editsSummary', "Made changes.")); + } else { + this.domNode = $('div'); + } + + // TODO@roblourens this case is now handled outside this Part in ChatListRenderer, but can it be cleaned up? + // return; + } else { + + + const cts = new CancellationTokenSource(); + + let isDisposed = false; + this._register(toDisposable(() => { + isDisposed = true; + cts.dispose(true); + this.ref?.object.clearModel(); + })); + + this.ref = this._register(diffEditorPool.get()); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + this._register(this.ref.object.onDidChangeContentHeight(() => { + this._onDidChangeHeight.fire(); + })); + + const data: ICodeCompareBlockData = { + element, + edit: chatTextEdit, + diffData: (async () => { + + const ref = await this.textModelService.createModelReference(chatTextEdit.uri); + + if (isDisposed) { + ref.dispose(); + return; + } + + this._register(ref); + + const original = ref.object.textEditorModel; + let originalSha1: string = ''; + + if (chatTextEdit.state) { + originalSha1 = chatTextEdit.state.sha1; + } else { + const sha1 = new DefaultModelSHA1Computer(); + if (sha1.canComputeSHA1(original)) { + originalSha1 = sha1.computeSHA1(original); + chatTextEdit.state = { sha1: originalSha1, applied: 0 }; + } + } + + const modified = this.modelService.createModel( + createTextBufferFactoryFromSnapshot(original.createSnapshot()), + { languageId: original.getLanguageId(), onDidChange: Event.None }, + URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: original.uri.path, query: generateUuid() }), + false + ); + const modRef = await this.textModelService.createModelReference(modified.uri); + this._register(modRef); + + const editGroups: ISingleEditOperation[][] = []; + if (isResponseVM(element)) { + const chatModel = this.chatService.getSession(element.sessionId)!; + + for (const request of chatModel.getRequests()) { + if (!request.response) { + continue; + } + for (const item of request.response.response.value) { + if (item.kind !== 'textEditGroup' || item.state?.applied || !isEqual(item.uri, chatTextEdit.uri)) { + continue; + } + for (const group of item.edits) { + const edits = group.map(TextEdit.asEditOperation); + editGroups.push(edits); + } + } + if (request.response === element.model) { + break; + } + } + } + + for (const edits of editGroups) { + modified.pushEditOperations(null, edits, () => null); + } + + return { + modified, + original, + originalSha1 + } satisfies ICodeCompareBlockDiffData; + })() + }; + this.ref.object.render(data, currentWidth, cts.token); + + this.domNode = this.ref.object.element; + } + } + + layout(width: number): void { + this.ref?.object.layout(width); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'textEditGroup'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class DiffEditorPool extends Disposable { + + private readonly _pool: ResourcePool; + + public inUse(): Iterable { + return this._pool.inUse; + } + + constructor( + options: ChatEditorOptions, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode); + })); + } + + get(): IDisposableReference { + const codeBlock = this._pool.get(); + let stale = false; + return { + object: codeBlock, + isStale: () => stale, + dispose: () => { + codeBlock.reset(); + stale = true; + this._pool.release(codeBlock); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts new file mode 100644 index 00000000000..8742d2da602 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { FileKind, FileType } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; +import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; + +const $ = dom.$; + +export class ChatTreeContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public readonly onDidFocus: Event; + + private tree: WorkbenchCompressibleAsyncDataTree; + + constructor( + data: IChatResponseProgressFileTreeData, + element: ChatTreeItem, + treePool: TreePool, + treeDataIndex: number, + @IOpenerService private readonly openerService: IOpenerService + ) { + super(); + + const ref = this._register(treePool.get()); + this.tree = ref.object; + this.onDidFocus = this.tree.onDidFocus; + + this._register(this.tree.onDidOpen((e) => { + if (e.element && !('children' in e.element)) { + this.openerService.open(e.element.uri); + } + })); + this._register(this.tree.onDidChangeCollapseState(() => { + this._onDidChangeHeight.fire(); + })); + this._register(this.tree.onContextMenu((e) => { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + })); + + this.tree.setInput(data).then(() => { + if (!ref.isStale()) { + this.tree.layout(); + this._onDidChangeHeight.fire(); + } + }); + + this.domNode = this.tree.getHTMLElement().parentElement!; + } + + domFocus() { + this.tree.domFocus(); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'treeData'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class TreePool extends Disposable { + private _pool: ResourcePool>; + + public get inUse(): ReadonlySet> { + return this._pool.inUse; + } + + constructor( + private _onDidChangeVisibility: Event, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configService: IConfigurationService, + @IThemeService private readonly themeService: IThemeService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => this.treeFactory())); + } + + private treeFactory(): WorkbenchCompressibleAsyncDataTree { + const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); + + const container = $('.interactive-response-progress-tree'); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); + + const tree = this.instantiationService.createInstance( + WorkbenchCompressibleAsyncDataTree, + 'ChatListRenderer', + container, + new ChatListTreeDelegate(), + new ChatListTreeCompressionDelegate(), + [new ChatListTreeRenderer(resourceLabels, this.configService.getValue('explorer.decorations'))], + new ChatListTreeDataSource(), + { + collapseByDefault: () => false, + expandOnlyOnTwistieClick: () => false, + identityProvider: { + getId: (e: IChatResponseProgressFileTreeData) => e.uri.toString() + }, + accessibilityProvider: { + getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, + getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") + }, + alwaysConsumeMouseWheel: false + }); + + return tree; + } + + get(): IDisposableReference> { + const object = this._pool.get(); + let stale = false; + return { + object, + isStale: () => stale, + dispose: () => { + stale = true; + this._pool.release(object); + } + }; + } +} + +class ChatListTreeDelegate implements IListVirtualDelegate { + static readonly ITEM_HEIGHT = 22; + + getHeight(element: IChatResponseProgressFileTreeData): number { + return ChatListTreeDelegate.ITEM_HEIGHT; + } + + getTemplateId(element: IChatResponseProgressFileTreeData): string { + return 'chatListTreeTemplate'; + } +} + +class ChatListTreeCompressionDelegate implements ITreeCompressionDelegate { + isIncompressible(element: IChatResponseProgressFileTreeData): boolean { + return !element.children; + } +} + +interface IChatListTreeRendererTemplate { + templateDisposables: DisposableStore; + label: IResourceLabel; +} + +class ChatListTreeRenderer implements ICompressibleTreeRenderer { + templateId: string = 'chatListTreeTemplate'; + + constructor(private labels: ResourceLabels, private decorations: IFilesConfiguration['explorer']['decorations']) { } + + renderCompressedElements(element: ITreeNode, void>, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { + templateData.label.element.style.display = 'flex'; + const label = element.element.elements.map((e) => e.label); + templateData.label.setResource({ resource: element.element.elements[0].uri, name: label }, { + title: element.element.elements[0].label, + fileKind: element.children ? FileKind.FOLDER : FileKind.FILE, + extraClasses: ['explorer-item'], + fileDecorations: this.decorations + }); + } + renderTemplate(container: HTMLElement): IChatListTreeRendererTemplate { + const templateDisposables = new DisposableStore(); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); + return { templateDisposables, label }; + } + renderElement(element: ITreeNode, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { + templateData.label.element.style.display = 'flex'; + if (!element.children.length && element.element.type !== FileType.Directory) { + templateData.label.setFile(element.element.uri, { + fileKind: FileKind.FILE, + hidePath: true, + fileDecorations: this.decorations, + }); + } else { + templateData.label.setResource({ resource: element.element.uri, name: element.element.label }, { + title: element.element.label, + fileKind: FileKind.FOLDER, + fileDecorations: this.decorations + }); + } + } + disposeTemplate(templateData: IChatListTreeRendererTemplate): void { + templateData.templateDisposables.dispose(); + } +} + +class ChatListTreeDataSource implements IAsyncDataSource { + hasChildren(element: IChatResponseProgressFileTreeData): boolean { + return !!element.children; + } + + async getChildren(element: IChatResponseProgressFileTreeData): Promise> { + return element.children ?? []; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts new file mode 100644 index 00000000000..3fd0b9fb239 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.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 * as dom from 'vs/base/browser/dom'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Codicon } from 'vs/base/common/codicons'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; + +const $ = dom.$; + +export class ChatWarningContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + kind: 'info' | 'warning' | 'error', + content: IMarkdownString, + renderer: MarkdownRenderer, + ) { + super(); + + this.domNode = $('.chat-notification-widget'); + let icon; + let iconClass; + switch (kind) { + case 'warning': + icon = Codicon.warning; + iconClass = '.chat-warning-codicon'; + break; + case 'error': + icon = Codicon.error; + iconClass = '.chat-error-codicon'; + break; + case 'info': + icon = Codicon.info; + iconClass = '.chat-info-codicon'; + break; + } + this.domNode.appendChild($(iconClass, undefined, renderIcon(icon))); + const markdownContent = renderer.render(content); + this.domNode.appendChild(markdownContent.element); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'warning'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css new file mode 100644 index 00000000000..bb9d7bc7b7e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-confirmation-widget { + border: 1px solid var(--vscode-chat-requestBorder); + border-radius: 4px; + padding: 8px 12px 12px; +} + +.chat-confirmation-widget:not(:last-child) { + margin-bottom: 16px; +} + +.chat-confirmation-widget .chat-confirmation-widget-title { + font-weight: 600; +} + +.chat-confirmation-widget .chat-confirmation-widget-title p { + margin: 0 0 4px 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown p { + margin-top: 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown > :last-child { + margin-bottom: 0px; +} + +.chat-confirmation-widget .chat-confirmation-buttons-container { + display: flex; + gap: 8px; + margin-top: 13px; +} + +.chat-confirmation-widget.hideButtons .chat-confirmation-buttons-container { + display: none; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts deleted file mode 100644 index ea834161ce2..00000000000 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ /dev/null @@ -1,380 +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 { isNonEmptyArray } from 'vs/base/common/arrays'; -import { Codicon } from 'vs/base/common/codicons'; -import { DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { localize, localize2 } from 'vs/nls'; -import { registerAction2 } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; -import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views'; -import { getHistoryAction, getOpenChatEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; -import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; -import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContributionService, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; - -const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'interactiveSession', - jsonSchema: { - description: localize('vscode.extension.contributes.interactiveSession', 'Contributes an Interactive Session provider'), - type: 'array', - items: { - additionalProperties: false, - type: 'object', - defaultSnippets: [{ body: { id: '', program: '', runtime: '' } }], - required: ['id', 'label'], - properties: { - id: { - description: localize('vscode.extension.contributes.interactiveSession.id', "Unique identifier for this Interactive Session provider."), - type: 'string' - }, - label: { - description: localize('vscode.extension.contributes.interactiveSession.label', "Display name for this Interactive Session provider."), - type: 'string' - }, - icon: { - description: localize('vscode.extension.contributes.interactiveSession.icon', "An icon for this Interactive Session provider."), - type: 'string' - }, - when: { - description: localize('vscode.extension.contributes.interactiveSession.when', "A condition which must be true to enable this Interactive Session provider."), - type: 'string' - }, - } - } - }, - activationEventsGenerator: (contributions: IRawChatProviderContribution[], result: { push(item: string): void }) => { - for (const contrib of contributions) { - result.push(`onInteractiveSession:${contrib.id}`); - } - }, -}); - -const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'chatParticipants', - jsonSchema: { - description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a chat participant'), - type: 'array', - items: { - additionalProperties: false, - type: 'object', - defaultSnippets: [{ body: { name: '', description: '' } }], - required: ['name', 'id'], - properties: { - id: { - description: localize('chatParticipantId', "A unique id for this chat participant."), - type: 'string' - }, - name: { - description: localize('chatParticipantName', "User-facing display name for this chat participant. The user will use '@' with this name to invoke the participant."), - type: 'string' - }, - description: { - description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."), - type: 'string' - }, - isDefault: { - markdownDescription: localize('chatParticipantIsDefaultDescription', "**Only** allowed for extensions that have the `defaultChatParticipant` proposal."), - type: 'boolean', - }, - isSticky: { - description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), - type: 'boolean' - }, - defaultImplicitVariables: { - markdownDescription: '**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default', - type: 'array', - items: { - type: 'string' - } - }, - commands: { - markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), - type: 'array', - items: { - additionalProperties: false, - type: 'object', - defaultSnippets: [{ body: { name: '', description: '' } }], - required: ['name'], - properties: { - name: { - description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."), - type: 'string' - }, - description: { - description: localize('chatCommandDescription', "A description of this command."), - type: 'string' - }, - when: { - description: localize('chatCommandWhen', "A condition which must be true to enable this command."), - type: 'string' - }, - sampleRequest: { - description: localize('chatCommandSampleRequest', "When the user clicks this command in `/help`, this text will be submitted to this participant."), - type: 'string' - }, - isSticky: { - description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), - type: 'boolean' - }, - defaultImplicitVariables: { - markdownDescription: localize('defaultImplicitVariables', "**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default"), - type: 'array', - items: { - type: 'string' - } - }, - } - } - }, - locations: { - markdownDescription: localize('chatLocationsDescription', "Locations in which this chat participant is available."), - type: 'array', - default: ['panel'], - items: { - type: 'string', - enum: ['panel', 'terminal', 'notebook'] - } - - } - } - } - }, - activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { - for (const contrib of contributions) { - result.push(`onChatParticipant:${contrib.id}`); - } - }, -}); - -export class ChatExtensionPointHandler implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; - - private readonly disposables = new DisposableStore(); - private _welcomeViewDescriptor?: IViewDescriptor; - private _viewContainer: ViewContainer; - private _registrationDisposables = new Map(); - private _participantRegistrationDisposables = new DisposableMap(); - - constructor( - @IChatContributionService private readonly _chatContributionService: IChatContributionService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IProductService private readonly productService: IProductService, - @IContextKeyService private readonly contextService: IContextKeyService, - @ILogService private readonly logService: ILogService, - ) { - this._viewContainer = this.registerViewContainer(); - this.registerListeners(); - this.handleAndRegisterChatExtensions(); - } - - private registerListeners() { - this.contextService.onDidChangeContext(e => { - - if (!this.productService.chatWelcomeView) { - return; - } - - const showWelcomeViewConfigKey = 'workbench.chat.experimental.showWelcomeView'; - const keys = new Set([showWelcomeViewConfigKey]); - if (e.affectsSome(keys)) { - const contextKeyExpr = ContextKeyExpr.equals(showWelcomeViewConfigKey, true); - const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); - if (this.contextService.contextMatchesRules(contextKeyExpr)) { - const viewId = this._chatContributionService.getViewIdForProvider(this.productService.chatWelcomeView.welcomeViewId); - - this._welcomeViewDescriptor = { - id: viewId, - name: { original: this.productService.chatWelcomeView.welcomeViewTitle, value: this.productService.chatWelcomeView.welcomeViewTitle }, - containerIcon: this._viewContainer.icon, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ providerId: this.productService.chatWelcomeView.welcomeViewId }]), - canToggleVisibility: false, - canMoveView: true, - order: 100 - }; - viewsRegistry.registerViews([this._welcomeViewDescriptor], this._viewContainer); - - viewsRegistry.registerViewWelcomeContent(viewId, { - content: this.productService.chatWelcomeView.welcomeViewContent, - }); - } else if (this._welcomeViewDescriptor) { - viewsRegistry.deregisterViews([this._welcomeViewDescriptor], this._viewContainer); - } - } - }, null, this.disposables); - } - - private handleAndRegisterChatExtensions(): void { - chatExtensionPoint.setHandler((extensions, delta) => { - for (const extension of delta.added) { - const extensionDisposable = new DisposableStore(); - for (const providerDescriptor of extension.value) { - this.registerChatProvider(providerDescriptor); - this._chatContributionService.registerChatProvider(providerDescriptor); - } - this._registrationDisposables.set(extension.description.identifier.value, extensionDisposable); - } - - for (const extension of delta.removed) { - const registration = this._registrationDisposables.get(extension.description.identifier.value); - if (registration) { - registration.dispose(); - this._registrationDisposables.delete(extension.description.identifier.value); - } - - for (const providerDescriptor of extension.value) { - this._chatContributionService.deregisterChatProvider(providerDescriptor.id); - } - } - }); - - chatParticipantExtensionPoint.setHandler((extensions, delta) => { - for (const extension of delta.added) { - for (const providerDescriptor of extension.value) { - if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); - continue; - } - - if (providerDescriptor.defaultImplicitVariables && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); - continue; - } - - if (!providerDescriptor.id || !providerDescriptor.name) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`); - continue; - } - - this._participantRegistrationDisposables.set( - getParticipantKey(extension.description.identifier, providerDescriptor.name), - this._chatAgentService.registerAgent( - providerDescriptor.id, - { - extensionId: extension.description.identifier, - id: providerDescriptor.id, - description: providerDescriptor.description, - metadata: { - isSticky: providerDescriptor.isSticky, - }, - name: providerDescriptor.name, - isDefault: providerDescriptor.isDefault, - defaultImplicitVariables: providerDescriptor.defaultImplicitVariables, - locations: isNonEmptyArray(providerDescriptor.locations) ? - providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : - [ChatAgentLocation.Panel], - slashCommands: providerDescriptor.commands ?? [] - } satisfies IChatAgentData)); - } - } - - for (const extension of delta.removed) { - for (const providerDescriptor of extension.value) { - this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.name)); - } - } - }); - } - - private registerViewContainer(): ViewContainer { - // Register View Container - const title = localize2('chat.viewContainer.label', "Chat"); - const icon = Codicon.commentDiscussion; - const viewContainerId = CHAT_SIDEBAR_PANEL_ID; - const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: viewContainerId, - title, - icon, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]), - storageId: viewContainerId, - hideIfEmpty: true, - order: 100, - }, ViewContainerLocation.Sidebar); - - return viewContainer; - } - - private registerChatProvider(providerDescriptor: IRawChatProviderContribution): IDisposable { - // Register View - const viewId = this._chatContributionService.getViewIdForProvider(providerDescriptor.id); - const viewDescriptor: IViewDescriptor[] = [{ - id: viewId, - containerIcon: this._viewContainer.icon, - containerTitle: this._viewContainer.title.value, - singleViewPaneContainerTitle: this._viewContainer.title.value, - name: { value: providerDescriptor.label, original: providerDescriptor.label }, - canToggleVisibility: false, - canMoveView: true, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ providerId: providerDescriptor.id }]), - when: ContextKeyExpr.deserialize(providerDescriptor.when) - }]; - Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); - - // Per-provider actions - - // Actions in view title - const disposables = new DisposableStore(); - disposables.add(registerAction2(getHistoryAction(viewId, providerDescriptor.id))); - disposables.add(registerAction2(getNewChatAction(viewId, providerDescriptor.id))); - disposables.add(registerAction2(getMoveToEditorAction(viewId, providerDescriptor.id))); - disposables.add(registerAction2(getMoveToNewWindowAction(viewId, providerDescriptor.id))); - - // "Open Chat" Actions - disposables.add(registerAction2(getOpenChatEditorAction(providerDescriptor.id, providerDescriptor.label, providerDescriptor.when))); - disposables.add(registerAction2(getQuickChatActionForProvider(providerDescriptor.id, providerDescriptor.label))); - - return { - dispose: () => { - Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); - Registry.as(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(this._viewContainer); - disposables.dispose(); - } - }; - } -} - -registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); - -function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { - return `${extensionId.value}_${participantName}`; -} - -export class ChatContributionService implements IChatContributionService { - declare _serviceBrand: undefined; - - private _registeredProviders = new Map(); - - constructor( - ) { } - - public getViewIdForProvider(providerId: string): string { - return ChatViewPane.ID + '.' + providerId; - } - - public registerChatProvider(provider: IChatProviderContribution): void { - this._registeredProviders.set(provider.id, provider); - } - - public deregisterChatProvider(providerId: string): void { - this._registeredProviders.delete(providerId); - } - - public get registeredProviders(): IChatProviderContribution[] { - return Array.from(this._registeredProviders.values()); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 34f10a76088..94fe53297a8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -18,13 +18,14 @@ import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { Memento } from 'vs/workbench/common/memento'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { IChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatModel, IExportableChatData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; export interface IChatEditorOptions extends IEditorOptions { - target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData }; + target?: { sessionId: string } | { data: IExportableChatData | ISerializableChatData }; } export class ChatEditor extends EditorPane { @@ -49,13 +50,15 @@ export class ChatEditor extends EditorPane { super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } - public async clear() { - return this.instantiationService.invokeFunction(clearChatEditor); + private async clear() { + if (this.input) { + return this.instantiationService.invokeFunction(clearChatEditor, this.input as ChatEditorInput); + } } protected override createEditor(parent: HTMLElement): void { this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); this.widget = this._register( scopedInstantiationService.createInstance( @@ -74,6 +77,12 @@ export class ChatEditor extends EditorPane { this.widget.setVisible(true); } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + + this.widget?.setVisible(visible); + } + public override focus(): void { super.focus(); @@ -101,7 +110,7 @@ export class ChatEditor extends EditorPane { } private updateModel(model: IChatModel, viewState?: IChatViewState): void { - this._memento = new Memento('interactive-session-editor-' + model.providerId, this.storageService); + this._memento = new Memento('interactive-session-editor-' + CHAT_PROVIDER_ID, this.storageService); this._viewState = viewState ?? this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IChatViewState; this.widget.setModel(model, { ...this._viewState }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts index e6cc6c81ff0..626f7b59f88 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts @@ -16,6 +16,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import type { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -29,7 +30,6 @@ export class ChatEditorInput extends EditorInput { private readonly inputCount: number; public sessionId: string | undefined; - public providerId: string | undefined; private model: IChatModel | undefined; @@ -59,8 +59,9 @@ export class ChatEditorInput extends EditorInput { throw new Error('Invalid chat URI'); } - this.sessionId = 'sessionId' in options.target ? options.target.sessionId : undefined; - this.providerId = 'providerId' in options.target ? options.target.providerId : undefined; + this.sessionId = (options.target && 'sessionId' in options.target) ? + options.target.sessionId : + undefined; this.inputCount = ChatEditorInput.getNextCount(); ChatEditorInput.countsInUse.add(this.inputCount); this._register(toDisposable(() => ChatEditorInput.countsInUse.delete(this.inputCount))); @@ -93,8 +94,8 @@ export class ChatEditorInput extends EditorInput { override async resolve(): Promise { if (typeof this.sessionId === 'string') { this.model = this.chatService.getOrRestoreSession(this.sessionId); - } else if (typeof this.providerId === 'string') { - this.model = this.chatService.startSession(this.providerId, CancellationToken.None); + } else if (!this.options.target) { + this.model = this.chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None); } else if ('data' in this.options.target) { this.model = this.chatService.loadSessionFromContent(this.options.target.data); } @@ -104,7 +105,6 @@ export class ChatEditorInput extends EditorInput { } this.sessionId = this.model.sessionId; - this.providerId = this.model.providerId; this._register(this.model.onDidChange(() => this._onDidChangeLabel.fire())); return this._register(new ChatEditorModel(this.model)); diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index 29a5ba75b7e..4219221b228 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -8,22 +8,19 @@ import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { formatChatQuestion } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; -import { IInlineChatFollowup } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; const $ = dom.$; -export class ChatFollowups extends Disposable { +export class ChatFollowups extends Disposable { constructor( container: HTMLElement, followups: T[], private readonly location: ChatAgentLocation, private readonly options: IButtonStyles | undefined, private readonly clickHandler: (followup: T) => void, - @IContextKeyService private readonly contextService: IContextKeyService, @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -34,49 +31,30 @@ export class ChatFollowups extend private renderFollowup(container: HTMLElement, followup: T): void { - if (followup.kind === 'command' && followup.when && !this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(followup.when))) { - return; - } - if (!this.chatAgentService.getDefaultAgent(this.location)) { // No default agent yet, which affects how followups are rendered, so can't render this yet return; } - let tooltipPrefix = ''; - if ('agentId' in followup && followup.agentId && followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { - const agent = this.chatAgentService.getAgent(followup.agentId); - if (!agent) { - // Refers to agent that doesn't exist - return; - } - - tooltipPrefix += `${chatAgentLeader}${agent.name} `; - if ('subCommand' in followup && followup.subCommand) { - tooltipPrefix += `${chatSubcommandLeader}${followup.subCommand} `; - } + const tooltipPrefix = formatChatQuestion(this.chatAgentService, this.location, '', followup.agentId, followup.subCommand); + if (tooltipPrefix === undefined) { + return; } const baseTitle = followup.kind === 'reply' ? (followup.title || followup.message) : followup.title; - - const tooltip = tooltipPrefix + - ('tooltip' in followup && followup.tooltip || baseTitle); - const button = this._register(new Button(container, { ...this.options, supportIcons: true, title: tooltip })); + const message = followup.kind === 'reply' ? followup.message : followup.title; + const tooltip = (tooltipPrefix + + ('tooltip' in followup && followup.tooltip || message)).trim(); + const button = this._register(new Button(container, { ...this.options, title: tooltip })); if (followup.kind === 'reply') { button.element.classList.add('interactive-followup-reply'); } else if (followup.kind === 'command') { button.element.classList.add('interactive-followup-command'); } - button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", followup.title); - let label = ''; - if (followup.kind === 'reply') { - label = '$(sparkle) ' + baseTitle; - } else { - label = baseTitle; - } - button.label = new MarkdownString(label, { supportThemeIcons: true }); + button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", baseTitle); + button.label = new MarkdownString(baseTitle); this._register(button.onDidClick(() => this.clickHandler(followup))); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f11c2850463..a96c6ab2995 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -6,13 +6,16 @@ import * as dom from 'vs/base/browser/dom'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; +import { Button } from 'vs/base/browser/ui/button/button'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; -import { HistoryNavigator } from 'vs/base/common/history'; +import { HistoryNavigator2 } from 'vs/base/common/history'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { basename, dirname } from 'vs/base/common/path'; import { isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; @@ -20,9 +23,10 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IDimension } from 'vs/editor/common/core/dimension'; import { IPosition } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hover'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; @@ -32,26 +36,27 @@ import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { FileKind } from 'vs/platform/files/common/files'; import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { asCssVariableWithDefault, checkboxBorder, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { ChatSubmitSecondaryAgentEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { CancelAction, IChatExecuteActionContext, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { CancelAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; -import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; const $ = dom.$; @@ -84,24 +89,33 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidBlur = this._register(new Emitter()); readonly onDidBlur = this._onDidBlur.event; + private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>()); + readonly onDidChangeContext = this._onDidChangeContext.event; + private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; + public get attachedContext(): ReadonlySet { + return this._attachedContext; + } + + private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; + private readonly _attachedContext = new Set(); + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + + private readonly inputEditorMaxHeight: number; private inputEditorHeight = 0; private container!: HTMLElement; private inputSideToolbarContainer?: HTMLElement; private followupsContainer!: HTMLElement; - private followupsDisposables = this._register(new DisposableStore()); + private readonly followupsDisposables = this._register(new DisposableStore()); - private implicitContextContainer!: HTMLElement; - private implicitContextLabel!: HTMLElement; - private implicitContextCheckbox!: Checkbox; - private implicitContextSettingEnabled = false; - get implicitContextEnabled() { - return this.implicitContextCheckbox.checked; - } + private attachedContextContainer!: HTMLElement; + private readonly attachedContextDisposables = this._register(new DisposableStore()); private _inputPartHeight: number = 0; get inputPartHeight() { @@ -117,16 +131,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._inputEditor; } - private history: HistoryNavigator; + private history: HistoryNavigator2; private historyNavigationBackwardsEnablement!: IContextKey; private historyNavigationForewardsEnablement!: IContextKey; - private onHistoryEntry = false; private inHistoryNavigation = false; private inputModel: ITextModel | undefined; private inputEditorHasText: IContextKey; private chatCursorAtTop: IContextKey; private inputEditorHasFocus: IContextKey; - private providerId: string | undefined; private cachedDimensions: dom.Dimension | undefined; private cachedToolbarWidth: number | undefined; @@ -137,6 +149,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // private readonly editorOptions: ChatEditorOptions, // TODO this should be used private readonly location: ChatAgentLocation, private readonly options: IChatInputPartOptions, + private readonly getInputState: () => any, @IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService, @IModelService private readonly modelService: IModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -144,28 +157,35 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ILogService private readonly logService: ILogService, ) { super(); + this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT; + this.inputEditorHasText = CONTEXT_CHAT_INPUT_HAS_TEXT.bindTo(contextKeyService); this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService); this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService); - this.history = new HistoryNavigator([], 5); - this._register(this.historyService.onDidClearHistory(() => this.history.clear())); + this.history = this.loadHistory(); + this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn))); - this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { this.inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); } - - if (e.affectsConfiguration('chat.experimental.implicitContext')) { - this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); - } })); } + private loadHistory(): HistoryNavigator2 { + const history = this.historyService.getHistory(this.location); + if (history.length === 0) { + history.push({ text: '' }); + } + + return new HistoryNavigator2(history, 50, historyKeyFn); + } + private _getAriaLabel(): string { const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat); if (verbose) { @@ -175,14 +195,39 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return localize('chatInput', "Chat Input"); } - setState(providerId: string, inputValue: string | undefined): void { - this.providerId = providerId; - const history = this.historyService.getHistory(providerId); - this.history = new HistoryNavigator(history, 50); - - if (typeof inputValue === 'string') { - this.setValue(inputValue); + updateState(inputState: Object): void { + if (this.inHistoryNavigation) { + return; } + + const newEntry = { text: this._inputEditor.getValue(), state: inputState }; + + if (this.history.isAtEnd()) { + // The last history entry should always be the current input value + this.history.replaceLast(newEntry); + } else { + // Added a reference while in the middle of history navigation, it's a new entry + this.history.replaceLast(newEntry); + this.history.resetCursor(); + } + } + + initForNewChatModel(inputValue: string | undefined, inputState: Object): void { + this.history = this.loadHistory(); + this.history.add({ text: inputValue ?? this.history.current().text, state: inputState }); + + if (inputValue) { + this.setValue(inputValue, false); + } + } + + logInputHistory(): void { + const historyStr = [...this.history].map(entry => JSON.stringify(entry)).join('\n'); + this.logService.info(`[${this.location}] Chat input history:`, historyStr); + } + + setVisible(visible: boolean): void { + this._onDidChangeVisibility.fire(visible); } get element(): HTMLElement { @@ -190,24 +235,41 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } showPreviousValue(): void { + const inputState = this.getInputState(); + if (this.history.isAtEnd()) { + this.saveCurrentValue(inputState); + } else { + if (!this.history.has({ text: this._inputEditor.getValue(), state: inputState })) { + this.saveCurrentValue(inputState); + this.history.resetCursor(); + } + } + this.navigateHistory(true); } showNextValue(): void { + const inputState = this.getInputState(); + if (this.history.isAtEnd()) { + return; + } else { + if (!this.history.has({ text: this._inputEditor.getValue(), state: inputState })) { + this.saveCurrentValue(inputState); + this.history.resetCursor(); + } + } + this.navigateHistory(false); } private navigateHistory(previous: boolean): void { - const historyEntry = (previous ? - (this.history.previous() ?? this.history.first()) : this.history.next()) - ?? { text: '' }; - - this.onHistoryEntry = previous || this.history.current() !== null; + const historyEntry = previous ? + this.history.previous() : this.history.next(); aria.status(historyEntry.text); this.inHistoryNavigation = true; - this.setValue(historyEntry.text); + this.setValue(historyEntry.text, true); this.inHistoryNavigation = false; this._onDidLoadInputState.fire(historyEntry.state); @@ -223,10 +285,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - setValue(value: string): void { + setValue(value: string, transient: boolean): void { this.inputEditor.setValue(value); // always leave cursor at the end this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); + + if (!transient) { + this.saveCurrentValue(this.getInputState()); + } + } + + private saveCurrentValue(inputState: any): void { + const newEntry = { text: this._inputEditor.getValue(), state: inputState }; + this.history.replaceLast(newEntry); } focus() { @@ -241,17 +312,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * Reset the input and update history. * @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed. */ - async acceptInput(userQuery?: string, inputState?: any): Promise { - if (userQuery) { - let element = this.history.getHistory().find(candidate => candidate.text === userQuery); - if (!element) { - element = { text: userQuery, state: inputState }; - } else { - element.state = inputState; - } - this.history.add(element); + async acceptInput(isUserQuery?: boolean): Promise { + if (isUserQuery) { + const userQuery = this._inputEditor.getValue(); + const entry: IChatHistoryEntry = { text: userQuery, state: this.getInputState() }; + this.history.replaceLast(entry); + this.history.add({ text: '' }); } + // Clear attached context, fire event to clear input state, and clear the input editor + this._attachedContext.clear(); + this._onDidLoadInputState.fire({}); if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) { this._acceptInputForVoiceover(); } else { @@ -267,25 +338,47 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } // Remove the input editor from the DOM temporarily to prevent VoiceOver // from reading the cleared text (the request) to the user. - this._inputEditorElement.removeChild(domNode); + domNode.remove(); this._inputEditor.setValue(''); this._inputEditorElement.appendChild(domNode); this._inputEditor.focus(); } + attachContext(overwrite: boolean, ...contentReferences: IChatRequestVariableEntry[]): void { + const removed = []; + if (overwrite) { + removed.push(...Array.from(this._attachedContext)); + this._attachedContext.clear(); + } + + if (contentReferences.length > 0) { + for (const reference of contentReferences) { + this._attachedContext.add(reference); + } + } + + if (removed.length > 0 || contentReferences.length > 0) { + this.initAttachedContext(this.attachedContextContainer); + + if (!overwrite) { + this._onDidChangeContext.fire({ removed, added: contentReferences }); + } + } + } + render(container: HTMLElement, initialValue: string, widget: IChatWidget) { this.container = dom.append(container, $('.interactive-input-part')); this.container.classList.toggle('compact', this.options.renderStyle === 'compact'); this.followupsContainer = dom.append(this.container, $('.interactive-input-followups')); - this.implicitContextContainer = dom.append(this.container, $('.chat-implicit-context')); - this.initImplicitContext(this.implicitContextContainer); + this.attachedContextContainer = dom.append(this.container, $('.chat-attached-context')); + this.initAttachedContext(this.attachedContextContainer); const inputAndSideToolbar = dom.append(this.container, $('.interactive-input-and-side-toolbar')); const inputContainer = dom.append(inputAndSideToolbar, $('.interactive-input-and-execute-toolbar')); const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer)); CONTEXT_IN_CHAT_INPUT.bindTo(inputScopedContextKeyService).set(true); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService])); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService]))); const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; @@ -310,34 +403,23 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge insertMode: 'replace', }; options.scrollbar = { ...(options.scrollbar ?? {}), vertical: 'hidden' }; + options.stickyScroll = { enabled: false }; - this._inputEditorElement = dom.append(inputContainer, $('.interactive-input-editor')); + this._inputEditorElement = dom.append(inputContainer, $(chatInputEditorContainerSelector)); const editorOptions = getSimpleCodeEditorWidgetOptions(); editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([HoverController.ID])); this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); this._register(this._inputEditor.onDidChangeModelContent(() => { - const currentHeight = Math.min(this._inputEditor.getContentHeight(), INPUT_EDITOR_MAX_HEIGHT); + const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); if (currentHeight !== this.inputEditorHeight) { this.inputEditorHeight = currentHeight; this._onDidChangeHeight.fire(); } - // Only allow history navigation when the input is empty. - // (If this model change happened as a result of a history navigation, this is canceled out by a call in this.navigateHistory) const model = this._inputEditor.getModel(); const inputHasText = !!model && model.getValue().trim().length > 0; this.inputEditorHasText.set(inputHasText); - - // If the user is typing on a history entry, then reset the onHistoryEntry flag so that history navigation can be disabled - if (!this.inHistoryNavigation) { - this.onHistoryEntry = false; - } - - if (!this.onHistoryEntry) { - this.historyNavigationForewardsEnablement.set(!inputHasText); - this.historyNavigationBackwardsEnablement.set(!inputHasText); - } })); this._register(this._inputEditor.onDidFocusEditorText(() => { this.inputEditorHasFocus.set(true); @@ -350,20 +432,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidBlur.fire(); })); - this._register(this._inputEditor.onDidChangeCursorPosition(e => { - const model = this._inputEditor.getModel(); - if (!model) { - return; - } - - const atTop = e.position.column === 1 && e.position.lineNumber === 1; - this.chatCursorAtTop.set(atTop); - - if (this.onHistoryEntry) { - this.historyNavigationBackwardsEnablement.set(atTop); - this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model))); - } - })); this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputContainer, this.options.menus.executeToolbar, { telemetrySource: this.options.menus.telemetrySource, @@ -374,7 +442,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge actionViewItemProvider: (action, options) => { if (this.location === ChatAgentLocation.Panel) { if ((action.id === SubmitAction.ID || action.id === CancelAction.ID) && action instanceof MenuItemAction) { - const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined); + const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined); return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction); } } @@ -416,18 +484,93 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const lineNumber = this.inputModel.getLineCount(); this._inputEditor.setPosition({ lineNumber, column: this.inputModel.getLineMaxColumn(lineNumber) }); } + + const onDidChangeCursorPosition = () => { + const model = this._inputEditor.getModel(); + if (!model) { + return; + } + + const position = this._inputEditor.getPosition(); + if (!position) { + return; + } + + const atTop = position.column === 1 && position.lineNumber === 1; + this.chatCursorAtTop.set(atTop); + + this.historyNavigationBackwardsEnablement.set(atTop); + this.historyNavigationForewardsEnablement.set(position.equals(getLastPosition(model))); + }; + this._register(this._inputEditor.onDidChangeCursorPosition(e => onDidChangeCursorPosition())); + onDidChangeCursorPosition(); } - private initImplicitContext(container: HTMLElement) { - this.implicitContextCheckbox = new Checkbox('#selection', true, { ...defaultCheckboxStyles, checkboxBorder: asCssVariableWithDefault(checkboxBorder, inputBackground) }); - container.append(this.implicitContextCheckbox.domNode); - this.implicitContextLabel = dom.append(container, $('span.chat-implicit-context-label')); - this.implicitContextLabel.textContent = '#selection'; - } + private initAttachedContext(container: HTMLElement) { + const oldHeight = container.offsetHeight; + dom.clearNode(container); + this.attachedContextDisposables.clear(); + dom.setVisibility(Boolean(this.attachedContext.size), this.attachedContextContainer); + if (!this.attachedContext.size) { + this._indexOfLastAttachedContextDeletedWithKeyboard = -1; + } + [...this.attachedContext.values()].forEach((attachment, index) => { + const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); + const label = this._contextResourceLabels.create(widget, { supportIcons: true }); + const file = 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; + if (file && attachment.isFile) { + const fileBasename = basename(file.path); + const fileDirname = dirname(file.path); + const friendlyName = `${fileBasename} ${fileDirname}`; + const 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); - setImplicitContextKinds(kinds: string[]) { - dom.setVisibility(this.implicitContextSettingEnabled && kinds.length > 0, this.implicitContextContainer); - this.implicitContextLabel.textContent = localize('use', "Use") + ' ' + kinds.map(k => `#${k}`).join(', '); + label.setFile(file, { + fileKind: FileKind.FILE, + hidePath: true, + range, + }); + widget.ariaLabel = ariaLabel; + widget.tabIndex = 0; + } else { + const attachmentLabel = attachment.fullName ?? attachment.name; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; + label.setLabel(withIcon, undefined); + + widget.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + widget.tabIndex = 0; + } + + const clearButton = new Button(widget, { supportIcons: true }); + + // 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.attachedContext.size - 1)) { + clearButton.focus(); + } + + this.attachedContextDisposables.add(clearButton); + clearButton.icon = Codicon.close; + const disp = clearButton.onDidClick((e) => { + this._attachedContext.delete(attachment); + disp.dispose(); + + // 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; + } + } + + this._onDidChangeHeight.fire(); + this._onDidChangeContext.fire({ removed: [attachment] }); + }); + this.attachedContextDisposables.add(disp); + }); + + if (oldHeight !== container.offsetHeight) { + this._onDidChangeHeight.fire(); + } } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -440,6 +583,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (items && items.length > 0) { this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); } + this._onDidChangeHeight.fire(); + } + + get contentHeight(): number { + const data = this.getLayoutData(); + return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; } layout(height: number, width: number) { @@ -450,25 +599,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private previousInputEditorDimension: IDimension | undefined; private _layout(height: number, width: number, allowRecurse = true): void { - const followupsHeight = this.followupsContainer.offsetHeight; + this.initAttachedContext(this.attachedContextContainer); - const inputPartBorder = 0; - const inputPartHorizontalPadding = this.options.renderStyle === 'compact' ? 8 : 40; - const inputPartVerticalPadding = this.options.renderStyle === 'compact' ? 12 : 24; - const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), height - followupsHeight - inputPartVerticalPadding - inputPartBorder, INPUT_EDITOR_MAX_HEIGHT); - const implicitContextHeight = this.implicitContextContainer.offsetHeight; + const data = this.getLayoutData(); - const inputEditorBorder = 2; - this._inputPartHeight = followupsHeight + inputEditorHeight + inputPartVerticalPadding + inputPartBorder + inputEditorBorder + implicitContextHeight; + const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.inputPartVerticalPadding); - const editorBorder = 2; - const editorPadding = 12; - const executeToolbarWidth = this.cachedToolbarWidth = this.toolbar.getItemsWidth(); - const toolbarPadding = 4; - const sideToolbarWidth = this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0; + const followupsWidth = width - data.inputPartHorizontalPadding; + this.followupsContainer.style.width = `${followupsWidth}px`; + + this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); - const newEditorWidth = width - inputPartHorizontalPadding - editorBorder - editorPadding - executeToolbarWidth - sideToolbarWidth - toolbarPadding; + const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.editorPadding - data.executeToolbarWidth - data.sideToolbarWidth - data.toolbarPadding; const newDimension = { width: newEditorWidth, height: inputEditorHeight }; if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler @@ -483,12 +626,31 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + private getLayoutData() { + return { + inputEditorBorder: 2, + followupsHeight: this.followupsContainer.offsetHeight, + inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), + inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 8 : 40, + inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 24, + implicitContextHeight: this.attachedContextContainer.offsetHeight, + editorBorder: 2, + editorPadding: 12, + toolbarPadding: 4, + executeToolbarWidth: this.cachedToolbarWidth = this.toolbar.getItemsWidth(), + sideToolbarWidth: this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0, + }; + } + saveState(): void { - const inputHistory = this.history.getHistory(); - this.historyService.saveHistory(this.providerId!, inputHistory); + this.saveCurrentValue(this.getInputState()); + const inputHistory = [...this.history]; + this.historyService.saveHistory(this.location, inputHistory); } } +const historyKeyFn = (entry: IChatHistoryEntry) => JSON.stringify(entry); + function getLastPosition(model: ITextModel): IPosition { return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 }; } @@ -529,7 +691,7 @@ class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { const secondaryAgent = chatAgentService.getSecondaryAgent(); if (secondaryAgent) { secondary.forEach(a => { - if (a.id === ChatSubmitSecondaryAgentEditorAction.ID) { + if (a.id === ChatSubmitSecondaryAgentAction.ID) { a.label = localize('chat.submitToSecondaryAgent', "Send to @{0}", secondaryAgent.name); } @@ -543,3 +705,6 @@ class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { this._register(menu.onDidChange(() => setActions())); } } + +const chatInputEditorContainerSelector = '.interactive-input-editor'; +setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 6be79e4e362..2dbc20d8e56 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -4,83 +4,85 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { alert } from 'vs/base/browser/ui/aria/aria'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; -import { distinct } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; +import { coalesce, distinct } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; +import { FileAccess } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; -import { basename } from 'vs/base/common/path'; -import { basenameOrAuthority } from 'vs/base/common/resources'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { autorun } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; -import { Range } from 'vs/editor/common/core/range'; -import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { localize } from 'vs/nls'; -import { createActionViewItem, IMenuEntryActionViewItemOptions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { FileKind, FileType } from 'vs/platform/files/common/files'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { WorkbenchCompressibleAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatAttachmentsContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart'; +import { ChatCodeCitationContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCodeCitationContentPart'; +import { ChatCommandButtonContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart'; +import { ChatConfirmationContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { ChatMarkdownContentPart, EditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart'; +import { ChatProgressContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart'; +import { ChatCollapsibleListContentPart, CollapsibleListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; +import { ChatTaskContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart'; +import { ChatTextEditContentPart, DiffEditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart'; +import { ChatTreeContentPart, TreePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart'; +import { ChatWarningContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatCodeBlockContentProvider, CodeBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatCodeBlockContentProvider, CodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; -import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; -import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; +import { IChatRequestVariableEntry, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; +import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatAgentVoteDirection, IChatConfirmation, IChatContentReference, IChatFollowup, IChatTask, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; +import { annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; const $ = dom.$; interface IChatListItemTemplate { + currentElement?: ChatTreeItem; + renderedParts?: IChatContentPart[]; readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; readonly avatarContainer: HTMLElement; - readonly agentAvatarContainer: HTMLElement; readonly username: HTMLElement; readonly detail: HTMLElement; readonly value: HTMLElement; - readonly referencesListContainer: HTMLElement; readonly contextKeyService: IContextKeyService; + readonly instantiationService: IInstantiationService; readonly templateDisposables: IDisposable; readonly elementDisposables: DisposableStore; + readonly agentHover: ChatAgentHover; } interface IItemHeightChangeParams { @@ -88,7 +90,9 @@ interface IItemHeightChangeParams { height: number; } -const forceVerboseLayoutTracing = false; +const forceVerboseLayoutTracing = false + // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed + ; export interface IChatRendererDelegate { getListLength(): number; @@ -96,13 +100,6 @@ export interface IChatRendererDelegate { readonly onDidScroll?: Event; } -export interface IChatListItemRendererOptions { - readonly renderStyle?: 'default' | 'compact'; - readonly noHeader?: boolean; - readonly noPadding?: boolean; - readonly editableCodeBlock?: boolean; -} - export class ChatListItemRenderer extends Disposable implements ITreeRenderer { static readonly ID = 'item'; @@ -118,19 +115,21 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; + private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter(); + readonly onDidClickRerunWithAgentOrCommandDetection: Event = this._onDidClickRerunWithAgentOrCommandDetection.event; + protected readonly _onDidChangeItemHeight = this._register(new Emitter()); readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; private readonly _editorPool: EditorPool; + private readonly _diffEditorPool: DiffEditorPool; private readonly _treePool: TreePool; - private readonly _contentReferencesListPool: ContentReferencesListPool; + private readonly _contentReferencesListPool: CollapsibleListPool; private _currentLayoutWidth: number = 0; private _isVisible = true; private _onDidChangeVisibility = this._register(new Emitter()); - private _usedReferencesEnabled = false; - constructor( editorOptions: ChatEditorOptions, private readonly location: ChatAgentLocation, @@ -141,35 +140,28 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (e.affectsConfiguration('chat.experimental.usedReferences')) { - this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences') ?? true; - } - })); } get templateId(): string { return ChatListItemRenderer.ID; } - editorsInUse() { + editorsInUse(): Iterable { return this._editorPool.inUse(); } @@ -181,21 +173,19 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + if (isResponseVM(template.currentElement) && template.currentElement.agent && !template.currentElement.agent.isDefault) { + agentHover.setAgent(template.currentElement.agent.id); + return agentHover.domNode; + } + + return undefined; + }; + const hoverOptions = getChatAgentHoverOptions(() => isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService); + templateDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); + templateDisposables.add(dom.addDisposableListener(user, dom.EventType.KEY_DOWN, e => { + const ev = new StandardKeyboardEvent(e); + if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { + const content = hoverContent(); + if (content) { + this.hoverService.showHover({ content, target: user, trapFocus: true, actions: hoverOptions.actions }, true); + } + } else if (ev.equals(KeyCode.Escape)) { + this.hoverService.hideHover(); + } + })); + const template: IChatListItemTemplate = { avatarContainer, username, detail, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, instantiationService: scopedInstantiationService, agentHover }; return template; } @@ -288,6 +327,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { try { - if (this.doNextProgressiveRender(element, index, templateData, !!initial, progressiveRenderingDisposables)) { + if (this.doNextProgressiveRender(element, index, templateData, !!initial)) { timer.cancel(); } } catch (err) { // Kill the timer if anything went wrong, avoid getting stuck in a nasty rendering loop. timer.cancel(); - throw err; + this.logService.error(err); } }; timer.cancelAndSet(runProgressiveRender, 50, dom.getWindow(templateData.rowContainer)); runProgressiveRender(true); } else if (isResponseVM(element)) { - const renderableResponse = annotateSpecialMarkdownContent(element.response.value); - this.basicRenderElement(renderableResponse, element, index, templateData); + this.basicRenderElement(element, index, templateData); } else if (isRequestVM(element)) { - const markdown = 'message' in element.message ? - element.message.message : - this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message); - this.basicRenderElement([{ content: new MarkdownString(markdown), kind: 'markdownContent' }], element, index, templateData); + this.basicRenderElement(element, index, templateData); } else { this.renderWelcomeMessage(element, templateData); } } private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { - let progressMsg: string = ''; - if (element.agent && !element.agent.isDefault) { - let usingMsg = chatAgentLeader + element.agent.name; - if (element.slashCommand) { - usingMsg += ` ${chatSubcommandLeader}${element.slashCommand.name}`; - } + templateData.elementDisposables.add(autorun(reader => { + this._renderDetail(element, templateData); + })); + } - if (element.isComplete) { - progressMsg = localize('usedAgent', "used {0}", usingMsg); - } else { - progressMsg = localize('usingAgent', "using {0}", usingMsg); - } - } else if (element.agentOrSlashCommandDetected) { - const usingMsg: string[] = []; - if (element.agent && !element.agent.isDefault) { - usingMsg.push(chatAgentLeader + element.agent.name); - } - if (element.slashCommand) { - usingMsg.push(chatSubcommandLeader + element.slashCommand.name); - } - if (usingMsg.length) { - if (element.isComplete) { - progressMsg = localize('usedAgent', "used {0}", usingMsg.join(' ')); - } else { - progressMsg = localize('usingAgent', "using {0}", usingMsg.join(' ')); + private _renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { + + dom.clearNode(templateData.detail); + + if (element.agentOrSlashCommandDetected) { + const msg = element.slashCommand ? localize('usedAgentSlashCommand', "used {0} [[(rerun without)]]", `${chatSubcommandLeader}${element.slashCommand.name}`) : localize('usedAgent', "[[(rerun without)]]"); + dom.reset(templateData.detail, renderFormattedText(msg, { + className: 'agentOrSlashCommandDetected', + inline: true, + actionHandler: { + disposables: templateData.elementDisposables, + callback: (content) => { + this._onDidClickRerunWithAgentOrCommandDetection.fire(element); + }, } - } - } else if (!element.isComplete) { - progressMsg = GeneratingPhrase; - } + })); - templateData.detail.textContent = progressMsg; - if (element.agent) { - templateData.detail.title = progressMsg + (element.slashCommand?.description ? `\n${element.slashCommand.description}` : ''); - } else { - templateData.detail.title = ''; + } else if (!element.isComplete) { + templateData.detail.textContent = GeneratingPhrase; + } + } + + private renderConfirmationAction(element: IChatRequestViewModel, templateData: IChatListItemTemplate) { + dom.clearNode(templateData.detail); + if (element.confirmation) { + templateData.detail.textContent = localize('chatConfirmationAction', 'selected "{0}"', element.confirmation); } } private renderAvatar(element: ChatTreeItem, templateData: IChatListItemTemplate): void { - if (URI.isUri(element.avatarIcon)) { - const avatarImgIcon = dom.$('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); - templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); + const icon = isResponseVM(element) ? + this.getAgentIcon(element.agent?.metadata) : + (element.avatarIcon ?? Codicon.account); + if (icon instanceof URI) { + const avatarIcon = dom.$('img.icon'); + avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); + templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); } else { - const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const icon = element.avatarIcon ?? defaultIcon; const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } - - if (isResponseVM(element) && element.agent && !element.agent.isDefault) { - dom.show(templateData.agentAvatarContainer); - const icon = this.getAgentIcon(element.agent.metadata); - if (icon instanceof URI) { - const avatarIcon = dom.$('img.icon'); - avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); - templateData.agentAvatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); - } else if (icon) { - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); - templateData.agentAvatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); - } else { - dom.hide(templateData.agentAvatarContainer); - return; - } - - templateData.agentAvatarContainer.classList.toggle('complete', element.isComplete); - if (!element.agentAvatarHasBeenRendered && !element.isComplete) { - element.agentAvatarHasBeenRendered = true; - templateData.agentAvatarContainer.classList.remove('loading'); - templateData.elementDisposables.add(disposableTimeout(() => { - templateData.agentAvatarContainer.classList.toggle('loading', !element.isComplete); - }, 100)); - } else { - templateData.agentAvatarContainer.classList.toggle('loading', !element.isComplete); - } - } else { - dom.hide(templateData.agentAvatarContainer); - } } - private getAgentIcon(agent: IChatAgentMetadata): URI | ThemeIcon | undefined { - if (agent.themeIcon) { + private getAgentIcon(agent: IChatAgentMetadata | undefined): URI | ThemeIcon { + if (agent?.themeIcon) { return agent.themeIcon; + } else if (agent?.iconDark && this.themeService.getColorTheme().type === ColorScheme.DARK) { + return agent.iconDark; + } else if (agent?.icon) { + return agent.icon; } else { - return this.themeService.getColorTheme().type === ColorScheme.DARK && agent.iconDark ? agent.iconDark : - agent.icon; + return Codicon.copilot; } } - private basicRenderElement(value: ReadonlyArray, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { - const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete); + private basicRenderElement(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { + let value: IChatRendererContent[] = []; + if (isRequestVM(element) && !element.confirmation) { + const markdown = 'message' in element.message ? + element.message.message : + this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message); + value = [{ content: new MarkdownString(markdown), kind: 'markdownContent' }]; + } else if (isResponseVM(element)) { + if (element.contentReferences.length) { + value.push({ kind: 'references', references: element.contentReferences }); + } + value.push(...annotateSpecialMarkdownContent(element.response.value)); + if (element.codeCitations.length) { + value.push({ kind: 'codeCitations', citations: element.codeCitations }); + } + } dom.clearNode(templateData.value); - dom.clearNode(templateData.referencesListContainer); if (isResponseVM(element)) { this.renderDetail(element, templateData); } - this.renderContentReferencesIfNeeded(element, templateData, templateData.elementDisposables); - - let fileTreeIndex = 0; + const parts: IChatContentPart[] = []; value.forEach((data, index) => { - const result = data.kind === 'treeData' - ? this.renderTreeData(data.treeData, element, templateData, fileTreeIndex++) - : data.kind === 'markdownContent' - ? this.renderMarkdown(data.content, element, templateData, fillInIncompleteTokens) - : data.kind === 'progressMessage' && onlyProgressMessagesAfterI(value, index) ? this.renderProgressMessage(data, false) // TODO render command - : data.kind === 'command' ? this.renderCommandButton(element, data) - : undefined; - if (result) { - templateData.value.appendChild(result.element); - templateData.elementDisposables.add(result); + const context: IChatContentPartRenderContext = { + element, + index, + content: value, + preceedingContentParts: parts, + }; + const newPart = this.renderChatContentPart(data, templateData, context); + if (newPart) { + templateData.value.appendChild(newPart.domNode); + parts.push(newPart); } }); + if (templateData.renderedParts) { + dispose(templateData.renderedParts); + } + templateData.renderedParts = parts; - if (isResponseVM(element) && element.errorDetails?.message) { - const icon = element.errorDetails.responseIsFiltered ? Codicon.info : Codicon.error; - const errorDetails = dom.append(templateData.value, $('.interactive-response-error-details', undefined, renderIcon(icon))); - const renderedError = templateData.elementDisposables.add(this.renderer.render(new MarkdownString(element.errorDetails.message))); - errorDetails.appendChild($('span', undefined, renderedError.element)); + if (isRequestVM(element) && element.variables.length) { + const newPart = this.renderAttachments(element.variables, element.contentReferences, templateData); + if (newPart) { + templateData.value.appendChild(newPart.domNode); + templateData.elementDisposables.add(newPart); + } } - if (isResponseVM(element) && element.isComplete && element.response.value.length === 0) { - let madeChanges = false; - for (const item of element.edits.values()) { - if (item.length > 0) { - madeChanges = true; - break; - } - } - if (madeChanges) { - dom.append(templateData.value, $('.interactive-edits-summary', undefined, localize('editsSummary', "Made changes."))); - } + if (isResponseVM(element) && element.errorDetails?.message) { + const renderedError = this.instantiationService.createInstance(ChatWarningContentPart, element.errorDetails.responseIsFiltered ? 'info' : 'error', new MarkdownString(element.errorDetails.message), this.renderer); + templateData.elementDisposables.add(renderedError); + templateData.value.appendChild(renderedError.domNode); } const newHeight = templateData.rowContainer.offsetHeight; @@ -505,20 +522,31 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + // Have to recompute the height here because codeblock rendering is currently async and it may have changed. + // If it becomes properly sync, then this could be removed. + element.currentRenderedHeight = templateData.rowContainer.offsetHeight; disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: newHeight }); + this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); })); } } + private updateItemHeight(templateData: IChatListItemTemplate): void { + if (!templateData.currentElement) { + return; + } + + const newHeight = templateData.rowContainer.offsetHeight; + templateData.currentElement.currentRenderedHeight = newHeight; + this._onDidChangeItemHeight.fire({ element: templateData.currentElement, height: newHeight }); + } + private renderWelcomeMessage(element: IChatWelcomeMessageViewModel, templateData: IChatListItemTemplate) { dom.clearNode(templateData.value); - dom.clearNode(templateData.referencesListContainer); - dom.hide(templateData.referencesListContainer); - for (const item of element.content) { + element.content.forEach((item, i) => { if (Array.isArray(item)) { - const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); + const scopedInstaService = templateData.elementDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService]))); templateData.elementDisposables.add( scopedInstaService.createInstance, ChatFollowups>( ChatFollowups, @@ -528,19 +556,29 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickFollowup.fire(followup))); } else { - const result = this.renderMarkdown(item as IMarkdownString, element, templateData); - templateData.value.appendChild(result.element); + const context: IChatContentPartRenderContext = { + element, + index: i, + // NA for welcome msg + content: [], + preceedingContentParts: [] + }; + const result = this.renderMarkdown(item, templateData, context); + templateData.value.appendChild(result.domNode); templateData.elementDisposables.add(result); } - } + }); const newHeight = templateData.rowContainer.offsetHeight; const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; element.currentRenderedHeight = newHeight; if (fireEvent) { const disposable = templateData.elementDisposables.add(dom.scheduleAtNextAnimationFrame(dom.getWindow(templateData.value), () => { + // Have to recompute the height here because codeblock rendering is currently async and it may have changed. + // If it becomes properly sync, then this could be removed. + element.currentRenderedHeight = templateData.rowContainer.offsetHeight; disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: newHeight }); + this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); })); } } @@ -548,442 +586,144 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - const renderedPart = renderedParts[index]; - // Is this part completely new? - if (!renderedPart) { - if (part.kind === 'treeData') { - partsToRender[index] = part.treeData; - } else if (part.kind === 'progressMessage') { - partsToRender[index] = { - progressMessage: part, - isAtEndOfResponse: onlyProgressMessagesAfterI(renderableResponse, index), - isLast: index === renderableResponse.length - 1, - } satisfies IChatProgressMessageRenderData; - } else if (part.kind === 'command') { - partsToRender[index] = part; - } else { - const wordCountResult = this.getDataForProgressiveRender(element, contentToMarkdown(part.content), { renderedWordCount: 0, lastRenderTime: 0 }); - if (wordCountResult !== undefined) { - this.traceLayout('doNextProgressiveRender', `Rendering new part ${index}, wordCountResult=${wordCountResult.actualWordCount}, rate=${wordCountResult.rate}`); - partsToRender[index] = { - renderedWordCount: wordCountResult.actualWordCount, - lastRenderTime: Date.now(), - isFullyRendered: wordCountResult.isFullString, - }; - wordCountResults[index] = wordCountResult; - } - } - } - - // Did this part's content change? - else if ((part.kind === 'markdownContent' || part.kind === 'progressMessage') && isMarkdownRenderData(renderedPart)) { // TODO - const wordCountResult = this.getDataForProgressiveRender(element, contentToMarkdown(part.content), renderedPart); - // Check if there are any new words to render - if (wordCountResult !== undefined && renderedPart.renderedWordCount !== wordCountResult?.actualWordCount) { - this.traceLayout('doNextProgressiveRender', `Rendering changed part ${index}, wordCountResult=${wordCountResult.actualWordCount}, rate=${wordCountResult.rate}`); - partsToRender[index] = { - renderedWordCount: wordCountResult.actualWordCount, - lastRenderTime: Date.now(), - isFullyRendered: wordCountResult.isFullString, - }; - wordCountResults[index] = wordCountResult; - } else if (!renderedPart.isFullyRendered && !wordCountResult) { - // This part is not fully rendered, but not enough time has passed to render more content - somePartIsNotFullyRendered = true; - } - } - - // Is it a progress message that needs to be rerendered? - else if (part.kind === 'progressMessage' && isProgressMessageRenderData(renderedPart) && ( - (renderedPart.isAtEndOfResponse !== onlyProgressMessagesAfterI(renderableResponse, index)) || - renderedPart.isLast !== (index === renderableResponse.length - 1))) { - partsToRender[index] = { - progressMessage: part, - isAtEndOfResponse: onlyProgressMessagesAfterI(renderableResponse, index), - isLast: index === renderableResponse.length - 1, - } satisfies IChatProgressMessageRenderData; - } - }); - - isFullyRendered = partsToRender.length === 0 && !somePartIsNotFullyRendered; - - if (isFullyRendered && element.isComplete) { - // Response is done and content is rendered, so do a normal render - this.traceLayout('runProgressiveRender', `end progressive render, index=${index} and clearing renderData, response is complete, index=${index}`); - element.renderData = undefined; - disposables.clear(); - this.basicRenderElement(renderableResponse, element, index, templateData); - } else if (!isFullyRendered) { - disposables.clear(); - this.renderContentReferencesIfNeeded(element, templateData, disposables); - let hasRenderedOneMarkdownBlock = false; - partsToRender.forEach((partToRender, index) => { - if (!partToRender) { - return; - } - - // Undefined => don't do anything. null => remove the rendered element - let result: { element: HTMLElement } & IDisposable | undefined | null; - if (isInteractiveProgressTreeData(partToRender)) { - result = this.renderTreeData(partToRender, element, templateData, index); - } else if (isProgressMessageRenderData(partToRender)) { - if (onlyProgressMessageRenderDatasAfterI(partsToRender, index)) { - result = this.renderProgressMessage(partToRender.progressMessage, index === partsToRender.length - 1); - } else { - result = null; - } - } else if (isCommandButtonRenderData(partToRender)) { - result = this.renderCommandButton(element, partToRender); - } - - // Avoid doing progressive rendering for multiple markdown parts simultaneously - else if (!hasRenderedOneMarkdownBlock && wordCountResults[index]) { - const { value } = wordCountResults[index]; - result = this.renderMarkdown(new MarkdownString(value), element, templateData, true); - hasRenderedOneMarkdownBlock = true; - } - - if (result === undefined) { - return; - } - - // Doing the progressive render - renderedParts[index] = partToRender; - const existingElement = templateData.value.children[index]; - if (existingElement) { - if (result === null) { - templateData.value.replaceChild($('span.placeholder-for-deleted-thing'), existingElement); - } else { - templateData.value.replaceChild(result.element, existingElement); - } - } else if (result) { - templateData.value.appendChild(result.element); - } - - if (result) { - disposables.add(result); - } - }); - } else { - // Nothing new to render, not done, keep waiting - return false; - } + this.basicRenderElement(element, index, templateData); + return true; } - // Some render happened - update the height + let isFullyRendered = false; + this.traceLayout('doNextProgressiveRender', `START progressive render, index=${index}, renderData=${JSON.stringify(element.renderData)}`); + const contentForThisTurn = this.getNextProgressiveRenderContent(element); + const partsToRender = this.diff(templateData.renderedParts ?? [], contentForThisTurn, element); + isFullyRendered = partsToRender.every(part => part === null); + + if (isFullyRendered) { + if (element.isComplete) { + // Response is done and content is rendered, so do a normal render + this.traceLayout('doNextProgressiveRender', `END progressive render, index=${index} and clearing renderData, response is complete`); + element.renderData = undefined; + this.basicRenderElement(element, index, templateData); + return true; + } + + // Nothing new to render, not done, keep waiting + this.traceLayout('doNextProgressiveRender', 'caught up with the stream- no new content to render'); + return false; + } + + // Do an actual progressive render + this.traceLayout('doNextProgressiveRender', `doing progressive render, ${partsToRender.length} parts to render`); + this.renderChatContentDiff(partsToRender, contentForThisTurn, element, templateData); + const height = templateData.rowContainer.offsetHeight; element.currentRenderedHeight = height; if (!isInRenderElement) { this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); } - return isFullyRendered; + return false; } - private renderTreeData(data: IChatResponseProgressFileTreeData, element: ChatTreeItem, templateData: IChatListItemTemplate, treeDataIndex: number): { element: HTMLElement; dispose: () => void } { - const treeDisposables = new DisposableStore(); - const ref = treeDisposables.add(this._treePool.get()); - const tree = ref.object; - - treeDisposables.add(tree.onDidOpen((e) => { - if (e.element && !('children' in e.element)) { - this.openerService.open(e.element.uri); + private renderChatContentDiff(partsToRender: ReadonlyArray, contentForThisTurn: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { + const renderedParts = templateData.renderedParts ?? []; + templateData.renderedParts = renderedParts; + partsToRender.forEach((partToRender, index) => { + if (!partToRender) { + // null=no change + return; } - })); - treeDisposables.add(tree.onDidChangeCollapseState(() => { - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); - })); - treeDisposables.add(tree.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); - tree.setInput(data).then(() => { - if (!ref.isStale()) { - tree.layout(); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + const alreadyRenderedPart = templateData.renderedParts?.[index]; + if (alreadyRenderedPart) { + alreadyRenderedPart.dispose(); } - }); - if (isResponseVM(element)) { - const fileTreeFocusInfo = { - treeDataId: data.uri.toString(), - treeIndex: treeDataIndex, - focus() { - tree.domFocus(); - } + const preceedingContentParts = renderedParts.slice(0, index); + const context: IChatContentPartRenderContext = { + element, + content: contentForThisTurn, + preceedingContentParts, + index }; - - treeDisposables.add(tree.onDidFocus(() => { - this.focusedFileTreesByResponseId.set(element.id, fileTreeFocusInfo.treeIndex); - })); - - const fileTrees = this.fileTreesByResponseId.get(element.id) ?? []; - fileTrees.push(fileTreeFocusInfo); - this.fileTreesByResponseId.set(element.id, distinct(fileTrees, (v) => v.treeDataId)); - treeDisposables.add(toDisposable(() => this.fileTreesByResponseId.set(element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); - } - - return { - element: tree.getHTMLElement().parentElement!, - dispose: () => { - treeDisposables.dispose(); - } - }; - } - - private renderContentReferencesIfNeeded(element: ChatTreeItem, templateData: IChatListItemTemplate, disposables: DisposableStore): void { - dom.clearNode(templateData.referencesListContainer); - if (isResponseVM(element) && this._usedReferencesEnabled && element.contentReferences.length) { - dom.show(templateData.referencesListContainer); - const contentReferencesListResult = this.renderContentReferencesListData(element.contentReferences, element, templateData); - templateData.referencesListContainer.appendChild(contentReferencesListResult.element); - disposables.add(contentReferencesListResult); - } else { - dom.hide(templateData.referencesListContainer); - } - } - - private renderContentReferencesListData(data: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): { element: HTMLElement; dispose: () => void } { - const listDisposables = new DisposableStore(); - const referencesLabel = data.length > 1 ? - localize('usedReferencesPlural', "Used {0} references", data.length) : - localize('usedReferencesSingular', "Used {0} reference", 1); - const iconElement = $('.chat-used-context-icon'); - const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - const buttonElement = $('.chat-used-context-label', undefined); - - const collapseButton = listDisposables.add(new Button(buttonElement, { - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined - })); - const container = $('.chat-used-context', undefined, buttonElement); - collapseButton.label = referencesLabel; - collapseButton.element.append(iconElement); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - container.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - listDisposables.add(collapseButton.onDidClick(() => { - iconElement.classList.remove(...ThemeIcon.asClassNameArray(icon(element))); - element.usedReferencesExpanded = !element.usedReferencesExpanded; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - container.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - })); - - const ref = listDisposables.add(this._contentReferencesListPool.get()); - const list = ref.object; - container.appendChild(list.getHTMLElement().parentElement!); - - listDisposables.add(list.onDidOpen((e) => { - if (e.element) { - const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; - const uri = URI.isUri(uriOrLocation) ? uriOrLocation : - uriOrLocation?.uri; - if (uri) { - this.openerService.open( - uri, - { - fromUserGesture: true, - editorOptions: { - ...e.editorOptions, - ...{ - selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined - } - } - }); - } - } - })); - listDisposables.add(list.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); - - const maxItemsShown = 6; - const itemsShown = Math.min(data.length, maxItemsShown); - const height = itemsShown * 22; - list.layout(height); - list.getHTMLElement().style.height = `${height}px`; - list.splice(0, list.length, data); - - return { - element: container, - dispose: () => { - listDisposables.dispose(); - } - }; - } - - private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { - element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); - } - - private renderProgressMessage(progress: IChatProgressMessage, showSpinner: boolean): IMarkdownRenderResult { - if (showSpinner) { - // this step is in progress, communicate it to SR users - alert(progress.content.value); - } - const codicon = showSpinner ? ThemeIcon.modify(Codicon.sync, 'spin').id : Codicon.check.id; - const markdown = new MarkdownString(`$(${codicon}) ${progress.content.value}`, { - supportThemeIcons: true - }); - const result = this.renderer.render(markdown); - result.element.classList.add('progress-step'); - return result; - } - - private renderCommandButton(element: ChatTreeItem, commandButton: IChatCommandButton): IMarkdownRenderResult { - const container = $('.chat-command-button'); - const disposables = new DisposableStore(); - const enabled = !isResponseVM(element) || !element.isStale; - const tooltip = enabled ? - commandButton.command.tooltip : - localize('commandButtonDisabled', "Button not available in restored chat"); - const button = disposables.add(new Button(container, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); - button.label = commandButton.command.title; - button.enabled = enabled; - - // TODO still need telemetry for command buttons - disposables.add(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); - return { - dispose() { - disposables.dispose(); - }, - element: container - }; - } - - private renderMarkdown(markdown: IMarkdownString, element: ChatTreeItem, templateData: IChatListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult { - const disposables = new DisposableStore(); - - markdown = new MarkdownString(markdown.value, { - isTrusted: { - // Disable all other config options except isTrusted - enabledCommands: typeof markdown.isTrusted === 'object' ? markdown.isTrusted?.enabledCommands : [] ?? [] - } - }); - - // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering - const orderedDisposablesList: IDisposable[] = []; - const codeblocks: IChatCodeBlockInfo[] = []; - let codeBlockIndex = 0; - const result = this.renderer.render(markdown, { - fillInIncompleteTokens, - codeBlockRendererSync: (languageId, text) => { - const index = codeBlockIndex++; - let textModel: Promise; - let range: Range | undefined; - let vulns: readonly IMarkdownVulnerability[] | undefined; - if (equalsIgnoreCase(languageId, localFileLanguageId)) { + const newPart = this.renderChatContentPart(partToRender, templateData, context); + if (newPart) { + // Maybe the part can't be rendered in this context, but this shouldn't really happen + if (alreadyRenderedPart) { try { - const parsedBody = parseLocalFileData(text); - range = parsedBody.range && Range.lift(parsedBody.range); - textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object); - } catch (e) { - return $('div'); + // This method can throw HierarchyRequestError + alreadyRenderedPart.domNode.replaceWith(newPart.domNode); + } catch (err) { + this.logService.error('ChatListItemRenderer#renderChatContentDiff: error replacing part', err); } } else { - if (!isRequestVM(element) && !isResponseVM(element)) { - console.error('Trying to render code block in welcome', element.id, index); - return $('div'); - } - - const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; - const modelEntry = this.codeBlockModelCollection.get(sessionId, element, index); - if (!modelEntry) { - console.error('Trying to render code block without model', element.id, index); - return $('div'); - } - vulns = modelEntry.vulns; - textModel = modelEntry.model; + templateData.value.appendChild(newPart.domNode); } - const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns }); - - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - disposables.add(ref.object.onDidChangeContentHeight(() => { - ref.object.layout(this._currentLayoutWidth); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); - })); - - if (isResponseVM(element)) { - const info: IChatCodeBlockInfo = { - codeBlockIndex: index, - element, - focus() { - ref.object.focus(); - } - }; - codeblocks.push(info); - if (ref.object.uri) { - const uri = ref.object.uri; - this.codeBlocksByEditorUri.set(uri, info); - disposables.add(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); - } - } - orderedDisposablesList.push(ref); - return ref.object.element; - }, - asyncRenderCallback: () => this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }), + renderedParts[index] = newPart; + } else if (alreadyRenderedPart) { + alreadyRenderedPart.domNode.remove(); + } }); + } - if (isResponseVM(element)) { - this.codeBlocksByResponseId.set(element.id, codeblocks); - disposables.add(toDisposable(() => this.codeBlocksByResponseId.delete(element.id))); + /** + * Returns all content parts that should be rendered, and trimmed markdown content. We will diff this with the current rendered set. + */ + private getNextProgressiveRenderContent(element: IChatResponseViewModel): IChatRendererContent[] { + const data = this.getDataForProgressiveRender(element); + + const renderableResponse = annotateSpecialMarkdownContent(element.response.value); + + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} at ${data.rate} words/s, counting...`); + let numNeededWords = data.numWordsToRender; + const partsToRender: IChatRendererContent[] = []; + if (element.contentReferences.length) { + partsToRender.push({ kind: 'references', references: element.contentReferences }); } - this.markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element); - - orderedDisposablesList.reverse().forEach(d => disposables.add(d)); - return { - element: result.element, - dispose() { - result.dispose(); - disposables.dispose(); + for (let i = 0; i < renderableResponse.length; i++) { + const part = renderableResponse[i]; + if (numNeededWords <= 0) { + break; } - }; + + if (part.kind === 'markdownContent') { + const wordCountResult = getNWords(part.content.value, numNeededWords); + if (wordCountResult.isFullString) { + partsToRender.push(part); + } else { + partsToRender.push({ kind: 'markdownContent', content: new MarkdownString(wordCountResult.value, part.content) }); + } + + this.traceLayout('getNextProgressiveRenderContent', ` Chunk ${i}: Want to render ${numNeededWords} words and found ${wordCountResult.returnedWordCount} words. Total words in chunk: ${wordCountResult.totalWordCount}`); + numNeededWords -= wordCountResult.returnedWordCount; + } else { + partsToRender.push(part); + } + } + + const lastWordCount = element.contentUpdateTimings?.lastWordCount ?? 0; + const newRenderedWordCount = data.numWordsToRender - numNeededWords; + const bufferWords = lastWordCount - newRenderedWordCount; + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} words. Rendering ${newRenderedWordCount} words. Buffer: ${bufferWords} words`); + if (newRenderedWordCount > 0 && newRenderedWordCount !== element.renderData?.renderedWordCount) { + // Only update lastRenderTime when we actually render new content + element.renderData = { lastRenderTime: Date.now(), renderedWordCount: newRenderedWordCount, renderedParts: partsToRender }; + } + + return partsToRender; } - private renderCodeBlock(data: ICodeBlockData): IDisposableReference { - const ref = this._editorPool.get(); - const editorInfo = ref.object; - editorInfo.render(data, this._currentLayoutWidth, this.rendererOptions.editableCodeBlock); + private getDataForProgressiveRender(element: IChatResponseViewModel) { + const renderData = element.renderData ?? { lastRenderTime: 0, renderedWordCount: 0 }; - return ref; - } - - private getDataForProgressiveRender(element: IChatResponseViewModel, data: IMarkdownString, renderData: Pick): IWordCountResult & { rate: number } | undefined { const rate = this.getProgressiveRenderRate(element); const numWordsToRender = renderData.lastRenderTime === 0 ? 1 : @@ -991,17 +731,179 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, contentToRender: ReadonlyArray, element: ChatTreeItem): ReadonlyArray { + const diff: (IChatRendererContent | null)[] = []; + for (let i = 0; i < contentToRender.length; i++) { + const content = contentToRender[i]; + const renderedPart = renderedParts[i]; + + if (!renderedPart || !renderedPart.hasSameContent(content, contentToRender.slice(i + 1), element)) { + diff.push(content); + } else { + // null -> no change + diff.push(null); + } + } + + return diff; + } + + private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (content.kind === 'treeData') { + return this.renderTreeData(content, templateData, context); + } else if (content.kind === 'progressMessage') { + return this.instantiationService.createInstance(ChatProgressContentPart, content, this.renderer, context); + } else if (content.kind === 'progressTask') { + return this.renderProgressTask(content, templateData, context); + } else if (content.kind === 'command') { + return this.instantiationService.createInstance(ChatCommandButtonContentPart, content, context); + } else if (content.kind === 'textEditGroup') { + return this.renderTextEdit(context, content, templateData); + } else if (content.kind === 'confirmation') { + return this.renderConfirmation(context, content, templateData); + } else if (content.kind === 'warning') { + return this.instantiationService.createInstance(ChatWarningContentPart, 'warning', content.content, this.renderer); + } else if (content.kind === 'markdownContent') { + return this.renderMarkdown(content.content, templateData, context); + } else if (content.kind === 'references') { + return this.renderContentReferencesListData(content, undefined, context, templateData); + } else if (content.kind === 'codeCitations') { + return this.renderCodeCitationsListData(content, context, templateData); + } + + return undefined; + } + + private renderTreeData(content: IChatTreeData, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const data = content.treeData; + const treeDataIndex = context.preceedingContentParts.filter(part => part instanceof ChatTreeContentPart).length; + const treePart = this.instantiationService.createInstance(ChatTreeContentPart, data, context.element, this._treePool, treeDataIndex); + + treePart.addDisposable(treePart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + + if (isResponseVM(context.element)) { + const fileTreeFocusInfo = { + treeDataId: data.uri.toString(), + treeIndex: treeDataIndex, + focus() { + treePart.domFocus(); + } + }; + + // TODO@roblourens there's got to be a better way to navigate trees + treePart.addDisposable(treePart.onDidFocus(() => { + this.focusedFileTreesByResponseId.set(context.element.id, fileTreeFocusInfo.treeIndex); + })); + + const fileTrees = this.fileTreesByResponseId.get(context.element.id) ?? []; + fileTrees.push(fileTreeFocusInfo); + this.fileTreesByResponseId.set(context.element.id, distinct(fileTrees, (v) => v.treeDataId)); + treePart.addDisposable(toDisposable(() => this.fileTreesByResponseId.set(context.element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); + } + + return treePart; + } + + private renderContentReferencesListData(references: IChatReferences, labelOverride: string | undefined, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatCollapsibleListContentPart { + const referencesPart = this.instantiationService.createInstance(ChatCollapsibleListContentPart, references.references, labelOverride, context.element as IChatResponseViewModel, this._contentReferencesListPool); + referencesPart.addDisposable(referencesPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + + return referencesPart; + } + + private renderCodeCitationsListData(citations: IChatCodeCitations, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatCodeCitationContentPart { + const citationsPart = this.instantiationService.createInstance(ChatCodeCitationContentPart, citations, context); + return citationsPart; + } + + private renderProgressTask(task: IChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (!isResponseVM(context.element)) { + return; + } + + const taskPart = this.instantiationService.createInstance(ChatTaskContentPart, task, this._contentReferencesListPool, this.renderer, context); + taskPart.addDisposable(taskPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + return taskPart; + } + + private renderConfirmation(context: IChatContentPartRenderContext, confirmation: IChatConfirmation, templateData: IChatListItemTemplate): IChatContentPart { + const part = this.instantiationService.createInstance(ChatConfirmationContentPart, confirmation, context); + part.addDisposable(part.onDidChangeHeight(() => this.updateItemHeight(templateData))); + return part; + } + + private renderAttachments(variables: IChatRequestVariableEntry[], contentReferences: ReadonlyArray | undefined, templateData: IChatListItemTemplate) { + return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); + } + + private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { + const textEditPart = this.instantiationService.createInstance(ChatTextEditContentPart, chatTextEdit, context, this.rendererOptions, this._diffEditorPool, this._currentLayoutWidth); + textEditPart.addDisposable(textEditPart.onDidChangeHeight(() => { + textEditPart.layout(this._currentLayoutWidth); + this.updateItemHeight(templateData); + })); + + return textEditPart; + } + + private renderMarkdown(markdown: IMarkdownString, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const element = context.element; + const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete || !!element.renderData); + const codeBlockStartIndex = context.preceedingContentParts.reduce((acc, part) => acc + (part instanceof ChatMarkdownContentPart ? part.codeblocks.length : 0), 0); + const markdownPart = this.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.renderer, this._currentLayoutWidth, this.codeBlockModelCollection, this.rendererOptions); + markdownPart.addDisposable(markdownPart.onDidChangeHeight(() => { + markdownPart.layout(this._currentLayoutWidth); + this.updateItemHeight(templateData); + })); + + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id) ?? []; + this.codeBlocksByResponseId.set(element.id, codeBlocksByResponseId); + markdownPart.addDisposable(toDisposable(() => { + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id); + if (codeBlocksByResponseId) { + markdownPart.codeblocks.forEach((info, i) => delete codeBlocksByResponseId[codeBlockStartIndex + i]); + } + })); + + markdownPart.codeblocks.forEach((info, i) => { + codeBlocksByResponseId[codeBlockStartIndex + i] = info; + if (info.uri) { + const uri = info.uri; + this.codeBlocksByEditorUri.set(uri, info); + markdownPart.addDisposable(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); + } + }); + + return markdownPart; + } + disposeElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { + this.traceLayout('disposeElement', `Disposing element, index=${index}`); + + // We could actually reuse a template across a renderElement call? + if (templateData.renderedParts) { + try { + dispose(coalesce(templateData.renderedParts)); + templateData.renderedParts = undefined; + dom.clearNode(templateData.value); + } catch (err) { + throw err; + } + } + + templateData.currentElement = undefined; templateData.elementDisposables.clear(); } @@ -1040,379 +942,9 @@ export class ChatListDelegate implements IListVirtualDelegate { } } - -interface IDisposableReference extends IDisposable { - object: T; - isStale: () => boolean; -} - -class EditorPool extends Disposable { - - private readonly _pool: ResourcePool; - - public inUse(): Iterable { - return this._pool.inUse; - } - - constructor( - options: ChatEditorOptions, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => { - return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); - })); - } - - get(): IDisposableReference { - const codeBlock = this._pool.get(); - let stale = false; - return { - object: codeBlock, - isStale: () => stale, - dispose: () => { - codeBlock.reset(); - stale = true; - this._pool.release(codeBlock); - } - }; - } -} - -class TreePool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configService: IConfigurationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.treeFactory())); - } - - private treeFactory(): WorkbenchCompressibleAsyncDataTree { - const resourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility }); - - const container = $('.interactive-response-progress-tree'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const tree = >this.instantiationService.createInstance( - WorkbenchCompressibleAsyncDataTree, - 'ChatListRenderer', - container, - new ChatListTreeDelegate(), - new ChatListTreeCompressionDelegate(), - [new ChatListTreeRenderer(resourceLabels, this.configService.getValue('explorer.decorations'))], - new ChatListTreeDataSource(), - { - collapseByDefault: () => false, - expandOnlyOnTwistieClick: () => false, - identityProvider: { - getId: (e: IChatResponseProgressFileTreeData) => e.uri.toString() - }, - accessibilityProvider: { - getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, - getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") - }, - alwaysConsumeMouseWheel: false - }); - - return tree; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ContentReferencesListPool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.listFactory())); - } - - private listFactory(): WorkbenchList { - const resourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility }); - - const container = $('.chat-used-context-list'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const list = >this.instantiationService.createInstance( - WorkbenchList, - 'ChatListRenderer', - container, - new ContentReferencesListDelegate(), - [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], - { - alwaysConsumeMouseWheel: false, - accessibilityProvider: { - getAriaLabel: (element: IChatContentReference) => { - const reference = element.reference; - if ('variableName' in reference) { - return reference.variableName; - } else if (URI.isUri(reference)) { - return basename(reference.path); - } else { - return basename(reference.uri.path); - } - }, - - getWidgetAriaLabel: () => localize('usedReferences', "Used References") - }, - }); - - return list; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ContentReferencesListDelegate implements IListVirtualDelegate { - getHeight(element: IChatContentReference): number { - return 22; - } - - getTemplateId(element: IChatContentReference): string { - return ContentReferencesListRenderer.TEMPLATE_ID; - } -} - -interface IChatContentReferenceListTemplate { - label: IResourceLabel; - templateDisposables: IDisposable; -} - -class ContentReferencesListRenderer implements IListRenderer { - static TEMPLATE_ID = 'contentReferencesListRenderer'; - readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; - - constructor( - private labels: ResourceLabels, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - ) { } - - renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - - renderElement(data: IChatContentReference, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void { - const reference = data.reference; - templateData.label.element.style.display = 'flex'; - if ('variableName' in reference) { - if (reference.value) { - const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; - templateData.label.setResource( - { - resource: uri, - name: basenameOrAuthority(uri), - description: `#${reference.variableName}`, - range: 'range' in reference.value ? reference.value.range : undefined - }); - } else { - const variable = this.chatVariablesService.getVariable(reference.variableName); - templateData.label.setLabel(`#${reference.variableName}`, undefined, { title: variable?.description }); - } - } else { - const uri = 'uri' in reference ? reference.uri : reference; - if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { - templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: Codicon.globe }); - } else { - templateData.label.setFile(uri, { - fileKind: FileKind.FILE, - // Should not have this live-updating data on a historical reference - fileDecorations: { badges: false, colors: false }, - range: 'range' in reference ? reference.range : undefined - }); - } - } - } - - disposeTemplate(templateData: IChatContentReferenceListTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -class ResourcePool extends Disposable { - private readonly pool: T[] = []; - - private _inUse = new Set; - public get inUse(): ReadonlySet { - return this._inUse; - } - - constructor( - private readonly _itemFactory: () => T, - ) { - super(); - } - - get(): T { - if (this.pool.length > 0) { - const item = this.pool.pop()!; - this._inUse.add(item); - return item; - } - - const item = this._register(this._itemFactory()); - this._inUse.add(item); - return item; - } - - release(item: T): void { - this._inUse.delete(item); - this.pool.push(item); - } -} - class ChatVoteButton extends MenuEntryActionViewItem { override render(container: HTMLElement): void { super.render(container); container.classList.toggle('checked', this.action.checked); } } - -class ChatListTreeDelegate implements IListVirtualDelegate { - static readonly ITEM_HEIGHT = 22; - - getHeight(element: IChatResponseProgressFileTreeData): number { - return ChatListTreeDelegate.ITEM_HEIGHT; - } - - getTemplateId(element: IChatResponseProgressFileTreeData): string { - return 'chatListTreeTemplate'; - } -} - -class ChatListTreeCompressionDelegate implements ITreeCompressionDelegate { - isIncompressible(element: IChatResponseProgressFileTreeData): boolean { - return !element.children; - } -} - -interface IChatListTreeRendererTemplate { - templateDisposables: DisposableStore; - label: IResourceLabel; -} - -class ChatListTreeRenderer implements ICompressibleTreeRenderer { - templateId: string = 'chatListTreeTemplate'; - - constructor(private labels: ResourceLabels, private decorations: IFilesConfiguration['explorer']['decorations']) { } - - renderCompressedElements(element: ITreeNode, void>, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - const label = element.element.elements.map((e) => e.label); - templateData.label.setResource({ resource: element.element.elements[0].uri, name: label }, { - title: element.element.elements[0].label, - fileKind: element.children ? FileKind.FOLDER : FileKind.FILE, - extraClasses: ['explorer-item'], - fileDecorations: this.decorations - }); - } - renderTemplate(container: HTMLElement): IChatListTreeRendererTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - renderElement(element: ITreeNode, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - if (!element.children.length && element.element.type !== FileType.Directory) { - templateData.label.setFile(element.element.uri, { - fileKind: FileKind.FILE, - hidePath: true, - fileDecorations: this.decorations, - }); - } else { - templateData.label.setResource({ resource: element.element.uri, name: element.element.label }, { - title: element.element.label, - fileKind: FileKind.FOLDER, - fileDecorations: this.decorations - }); - } - } - disposeTemplate(templateData: IChatListTreeRendererTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -class ChatListTreeDataSource implements IAsyncDataSource { - hasChildren(element: IChatResponseProgressFileTreeData): boolean { - return !!element.children; - } - - async getChildren(element: IChatResponseProgressFileTreeData): Promise> { - return element.children ?? []; - } -} - -function isInteractiveProgressTreeData(item: Object): item is IChatResponseProgressFileTreeData { - return 'label' in item; -} - -function contentToMarkdown(str: string | IMarkdownString): IMarkdownString { - return typeof str === 'string' ? { value: str } : str; -} - -function isProgressMessage(item: any): item is IChatProgressMessage { - return item && 'kind' in item && item.kind === 'progressMessage'; -} - -function isProgressMessageRenderData(item: IChatRenderData): item is IChatProgressMessageRenderData { - return item && 'isAtEndOfResponse' in item; -} - -function isCommandButtonRenderData(item: IChatRenderData): item is IChatCommandButton { - return item && 'kind' in item && item.kind === 'command'; -} - -function isMarkdownRenderData(item: IChatRenderData): item is IChatResponseMarkdownRenderData { - return item && 'renderedWordCount' in item; -} - -function onlyProgressMessagesAfterI(items: ReadonlyArray, i: number): boolean { - return items.slice(i).every(isProgressMessage); -} - -function onlyProgressMessageRenderDatasAfterI(items: ReadonlyArray, i: number): boolean { - return items.slice(i).every(isProgressMessageRenderData); -} diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index bbacc39b7c8..c8207548700 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -4,23 +4,87 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { Lazy } from 'vs/base/common/lazy'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; import { Location } from 'vs/editor/common/languages'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { asCssVariable } from 'vs/platform/theme/common/colorUtils'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { contentRefUrl } from '../common/annotations'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -const variableRefUrl = 'http://_vscodedecoration_'; +/** For rendering slash commands, variables */ +const decorationRefUrl = `http://_vscodedecoration_`; + +/** For rendering agent decorations with hover */ +const agentRefUrl = `http://_chatagent_`; + +/** For rendering agent decorations with hover */ +const agentSlashRefUrl = `http://_chatslash_`; + +export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, accessor: ServicesAccessor): string { + const chatAgentNameService = accessor.get(IChatAgentNameService); + const chatAgentService = accessor.get(IChatAgentService); + + const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); + let name = `${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; + const isDupe = isAllowed && chatAgentService.agentHasDupeName(agent.id); + if (isDupe) { + name += ` (${agent.publisherDisplayName})`; + } + + const args: IAgentWidgetArgs = { agentId: agent.id, name, isClickable }; + return `[${agent.name}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; +} + +interface IAgentWidgetArgs { + agentId: string; + name: string; + isClickable?: boolean; +} + +export function agentSlashCommandToMarkdown(agent: IChatAgentData, command: IChatAgentCommand): string { + const text = `${chatSubcommandLeader}${command.name}`; + const args: ISlashCommandWidgetArgs = { agentId: agent.id, command: command.name }; + return `[${text}](${agentSlashRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; +} + +interface ISlashCommandWidgetArgs { + agentId: string; + command: string; +} + +interface IDecorationWidgetArgs { + title?: string; +} export class ChatMarkdownDecorationsRenderer { constructor( @IKeybindingService private readonly keybindingService: IKeybindingService, @ILabelService private readonly labelService: ILabelService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IHoverService private readonly hoverService: IHoverService, + @IChatService private readonly chatService: IChatService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService ) { } convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { @@ -28,28 +92,70 @@ export class ChatMarkdownDecorationsRenderer { for (const part of parsedRequest.parts) { if (part instanceof ChatRequestTextPart) { result += part.text; + } else if (part instanceof ChatRequestAgentPart) { + result += this.instantiationService.invokeFunction(accessor => agentToMarkdown(part.agent, false, accessor)); } else { - const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI) - || undefined; - const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : - part instanceof ChatRequestAgentPart ? part.agent.id : - ''; - - result += `[${part.text}](${variableRefUrl}?${title})`; + result += this.genericDecorationToMarkdown(part); } } return result; } - walkTreeAndAnnotateReferenceLinks(element: HTMLElement): void { + private genericDecorationToMarkdown(part: IParsedChatRequestPart): string { + const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? + part.data : + undefined; + const title = uri ? this.labelService.getUriLabel(uri, { relative: true }) : + part instanceof ChatRequestSlashCommandPart ? part.slashCommand.detail : + part instanceof ChatRequestAgentSubcommandPart ? part.command.description : + part instanceof ChatRequestVariablePart ? (this.chatVariablesService.getVariable(part.variableName)?.description ?? '') : + ''; + + const args: IDecorationWidgetArgs = { title }; + const text = part.text; + return `[${text}](${decorationRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; + } + + walkTreeAndAnnotateReferenceLinks(element: HTMLElement): IDisposable { + const store = new DisposableStore(); element.querySelectorAll('a').forEach(a => { const href = a.getAttribute('data-href'); if (href) { - if (href.startsWith(variableRefUrl)) { - const title = decodeURIComponent(href.slice(variableRefUrl.length + 1)); + if (href.startsWith(agentRefUrl)) { + let args: IAgentWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); + } catch (e) { + this.logService.error('Invalid chat widget render data JSON', toErrorMessage(e)); + } + + if (args) { + a.parentElement!.replaceChild( + this.renderAgentWidget(args, store), + a); + } + } else if (href.startsWith(agentSlashRefUrl)) { + let args: ISlashCommandWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); + } catch (e) { + this.logService.error('Invalid chat slash command render data JSON', toErrorMessage(e)); + } + + if (args) { + a.parentElement!.replaceChild( + this.renderSlashCommandWidget(a.textContent!, args, store), + a); + } + } else if (href.startsWith(decorationRefUrl)) { + let args: IDecorationWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(decorationRefUrl.length + 1))); + } catch (e) { } + a.parentElement!.replaceChild( - this.renderResourceWidget(a.textContent!, title), + this.renderResourceWidget(a.textContent!, args, store), a); } else if (href.startsWith(contentRefUrl)) { this.renderFileWidget(href, a); @@ -58,6 +164,63 @@ export class ChatMarkdownDecorationsRenderer { } } }); + + return store; + } + + private renderAgentWidget(args: IAgentWidgetArgs, store: DisposableStore): HTMLElement { + const nameWithLeader = `${chatAgentLeader}${args.name}`; + let container: HTMLElement; + if (args.isClickable) { + container = dom.$('span.chat-agent-widget'); + const button = store.add(new Button(container, { + buttonBackground: asCssVariable(chatSlashCommandBackground), + buttonForeground: asCssVariable(chatSlashCommandForeground), + buttonHoverBackground: undefined + })); + button.label = nameWithLeader; + store.add(button.onDidClick(() => { + const agent = this.chatAgentService.getAgent(args.agentId); + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !agent) { + return; + } + + this.chatService.sendRequest(widget.viewModel!.sessionId, agent.metadata.sampleRequest ?? '', { location: widget.location, agentId: agent.id }); + })); + } else { + container = this.renderResourceWidget(nameWithLeader, undefined, store); + } + + const agent = this.chatAgentService.getAgent(args.agentId); + const hover: Lazy = new Lazy(() => store.add(this.instantiationService.createInstance(ChatAgentHover))); + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), container, () => { + hover.value.setAgent(args.agentId); + return hover.value.domNode; + }, agent && getChatAgentHoverOptions(() => agent, this.commandService))); + return container; + } + + private renderSlashCommandWidget(name: string, args: ISlashCommandWidgetArgs, store: DisposableStore): HTMLElement { + const container = dom.$('span.chat-agent-widget.chat-command-widget'); + const agent = this.chatAgentService.getAgent(args.agentId); + const button = store.add(new Button(container, { + buttonBackground: asCssVariable(chatSlashCommandBackground), + buttonForeground: asCssVariable(chatSlashCommandForeground), + buttonHoverBackground: undefined + })); + button.label = name; + store.add(button.onDidClick(() => { + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !agent) { + return; + } + + const command = agent.slashCommands.find(c => c.name === args.command); + this.chatService.sendRequest(widget.viewModel!.sessionId, command?.sampleRequest ?? '', { location: widget.location, agentId: agent.id, slashCommand: args.command }); + })); + + return container; } private renderFileWidget(href: string, a: HTMLAnchorElement): void { @@ -86,10 +249,13 @@ export class ChatMarkdownDecorationsRenderer { } - private renderResourceWidget(name: string, title: string): HTMLElement { + private renderResourceWidget(name: string, args: IDecorationWidgetArgs | undefined, store: DisposableStore): HTMLElement { const container = dom.$('span.chat-resource-widget'); const alias = dom.$('span', undefined, name); - alias.title = title; + if (args?.title) { + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), container, args.title)); + } + container.appendChild(alias); return container; } diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts new file mode 100644 index 00000000000..98e84b995ad --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownRenderOptions, MarkedOptions } from 'vs/base/browser/markdownRenderer'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IMarkdownRendererOptions, IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; + +const allowedHtmlTags = [ + 'b', + 'blockquote', + 'br', + 'code', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'sub', + 'sup', + 'table', + 'tbody', + 'td', + 'th', + 'thead', + 'tr', + 'ul', + 'a', + 'img', + + // TODO@roblourens when we sanitize attributes in markdown source, we can ban these elements at that step. microsoft/vscode-copilot#5091 + // Not in the official list, but used for codicons and other vscode markdown extensions + 'span', + 'div', +]; + +/** + * This wraps the MarkdownRenderer and applies sanitizer options needed for Chat. + */ +export class ChatMarkdownRenderer extends MarkdownRenderer { + constructor( + options: IMarkdownRendererOptions | undefined, + @ILanguageService languageService: ILanguageService, + @IOpenerService openerService: IOpenerService, + @ITrustedDomainService private readonly trustedDomainService: ITrustedDomainService, + @IHoverService private readonly hoverService: IHoverService, + ) { + super(options ?? {}, languageService, openerService); + } + + override render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { + options = { + ...options, + remoteImageIsAllowed: (uri) => this.trustedDomainService.isValid(uri), + sanitizerOptions: { + replaceWithPlaintext: true, + allowedTags: allowedHtmlTags, + } + }; + + const mdWithBody: IMarkdownString | undefined = (markdown && markdown.supportHtml) ? + { + ...markdown, + + // dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this. + // The \n\n prevents marked.js from parsing the body contents as just text in an 'html' token, instead of actual markdown. + value: `\n\n${markdown.value}`, + } + : markdown; + const result = super.render(mdWithBody, options, markedOptions); + return this.attachCustomHover(result); + } + + private attachCustomHover(result: IMarkdownRenderResult): IMarkdownRenderResult { + const store = new DisposableStore(); + result.element.querySelectorAll('a').forEach((element) => { + if (element.title) { + const title = element.title; + element.title = ''; + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title)); + } + }); + + return { + element: result.element, + dispose: () => { + result.dispose(); + store.dispose(); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatOptions.ts b/src/vs/workbench/contrib/chat/browser/chatOptions.ts index e76519d260a..1995598eba9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatOptions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatOptions.ts @@ -79,7 +79,7 @@ export class ChatEditorOptions extends Disposable { private readonly resultEditorBackgroundColor: string, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeService private readonly themeService: IThemeService, - @IViewDescriptorService readonly viewDescriptorService: IViewDescriptorService + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService ) { super(); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts new file mode 100644 index 00000000000..aa461ef6572 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -0,0 +1,282 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from 'vs/base/common/actions'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { Codicon } from 'vs/base/common/codicons'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import * as strings from 'vs/base/common/strings'; +import { localize, localize2 } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views'; +import { CHAT_VIEW_ID } from 'vs/workbench/contrib/chat/browser/chat'; +import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'chatParticipants', + jsonSchema: { + description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a chat participant'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name', 'id'], + properties: { + id: { + description: localize('chatParticipantId', "A unique id for this chat participant."), + type: 'string' + }, + name: { + description: localize('chatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant. Name must not contain whitespace."), + type: 'string', + pattern: '^[\\w0-9_-]+$' + }, + fullName: { + markdownDescription: localize('chatParticipantFullName', "The full name of this chat participant, which is shown as the label for responses coming from this participant. If not provided, {0} is used.", '`name`'), + type: 'string' + }, + description: { + description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."), + type: 'string' + }, + isSticky: { + description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), + type: 'boolean' + }, + sampleRequest: { + description: localize('chatSampleRequest', "When the user clicks this participant in `/help`, this text will be submitted to the participant."), + type: 'string' + }, + when: { + description: localize('chatParticipantWhen', "A condition which must be true to enable this participant."), + type: 'string' + }, + commands: { + markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."), + type: 'string' + }, + description: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + when: { + description: localize('chatCommandWhen', "A condition which must be true to enable this command."), + type: 'string' + }, + sampleRequest: { + description: localize('chatCommandSampleRequest', "When the user clicks this command in `/help`, this text will be submitted to the participant."), + type: 'string' + }, + isSticky: { + description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), + type: 'boolean' + }, + } + } + }, + } + } + }, + activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { + for (const contrib of contributions) { + result.push(`onChatParticipant:${contrib.id}`); + } + }, +}); + +export class ChatCompatibilityNotifier implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.chatCompatNotifier'; + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @INotificationService notificationService: INotificationService, + @ICommandService commandService: ICommandService + ) { + // It may be better to have some generic UI for this, for any extension that is incompatible, + // but this is only enabled for Copilot Chat now and it needs to be obvious. + extensionsWorkbenchService.queryLocal().then(exts => { + const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat'); + if (chat?.local?.validations.some(v => v[0] === Severity.Error)) { + notificationService.notify({ + severity: Severity.Error, + message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), + actions: { + primary: [ + new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { + return commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); + }) + ] + } + }); + } + }); + } +} + +export class ChatExtensionPointHandler implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; + + private _viewContainer: ViewContainer; + private _participantRegistrationDisposables = new DisposableMap(); + + constructor( + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @ILogService private readonly logService: ILogService, + ) { + this._viewContainer = this.registerViewContainer(); + this.handleAndRegisterChatExtensions(); + } + + private handleAndRegisterChatExtensions(): void { + chatParticipantExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const providerDescriptor of extension.value) { + if (!providerDescriptor.name.match(/^[\w0-9_-]+$/)) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w0-9_-]+$/.`); + continue; + } + + if (providerDescriptor.fullName && strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter(providerDescriptor.fullName)) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains ambiguous characters: ${providerDescriptor.fullName}.`); + continue; + } + + // Spaces are allowed but considered "invisible" + if (providerDescriptor.fullName && strings.InvisibleCharacters.containsInvisibleCharacter(providerDescriptor.fullName.replace(/ /g, ''))) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains invisible characters: ${providerDescriptor.fullName}.`); + continue; + } + + if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); + continue; + } + + if ((providerDescriptor.defaultImplicitVariables || providerDescriptor.locations) && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); + continue; + } + + if (!providerDescriptor.id || !providerDescriptor.name) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`); + continue; + } + + const store = new DisposableStore(); + if (providerDescriptor.isDefault && (!providerDescriptor.locations || providerDescriptor.locations?.includes(ChatAgentLocation.Panel))) { + store.add(this.registerDefaultParticipantView(providerDescriptor)); + } + + store.add(this._chatAgentService.registerAgent( + providerDescriptor.id, + { + extensionId: extension.description.identifier, + publisherDisplayName: extension.description.publisherDisplayName ?? extension.description.publisher, // May not be present in OSS + extensionPublisherId: extension.description.publisher, + extensionDisplayName: extension.description.displayName ?? extension.description.name, + id: providerDescriptor.id, + description: providerDescriptor.description, + when: providerDescriptor.when, + metadata: { + isSticky: providerDescriptor.isSticky, + sampleRequest: providerDescriptor.sampleRequest, + }, + name: providerDescriptor.name, + fullName: providerDescriptor.fullName, + isDefault: providerDescriptor.isDefault, + locations: isNonEmptyArray(providerDescriptor.locations) ? + providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : + [ChatAgentLocation.Panel], + slashCommands: providerDescriptor.commands ?? [] + } satisfies IChatAgentData)); + + this._participantRegistrationDisposables.set( + getParticipantKey(extension.description.identifier, providerDescriptor.id), + store + ); + } + } + + for (const extension of delta.removed) { + for (const providerDescriptor of extension.value) { + this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.id)); + } + } + }); + } + + private registerViewContainer(): ViewContainer { + // Register View Container + const title = localize2('chat.viewContainer.label', "Chat"); + const icon = Codicon.commentDiscussion; + const viewContainerId = CHAT_SIDEBAR_PANEL_ID; + const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: viewContainerId, + title, + icon, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]), + storageId: viewContainerId, + hideIfEmpty: true, + order: 100, + }, ViewContainerLocation.Sidebar); + + return viewContainer; + } + + private hasRegisteredDefaultParticipantView = false; + private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { + if (this.hasRegisteredDefaultParticipantView) { + this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`); + return Disposable.None; + } + + // Register View + const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name; + const viewDescriptor: IViewDescriptor[] = [{ + id: CHAT_VIEW_ID, + containerIcon: this._viewContainer.icon, + containerTitle: this._viewContainer.title.value, + singleViewPaneContainerTitle: this._viewContainer.title.value, + name: { value: name, original: name }, + canToggleVisibility: false, + canMoveView: true, + ctorDescriptor: new SyncDescriptor(ChatViewPane), + }]; + this.hasRegisteredDefaultParticipantView = true; + Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); + + return toDisposable(() => { + this.hasRegisteredDefaultParticipantView = false; + Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); + }); + } +} + +function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { + return `${extensionId.value}_${participantName}`; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index 7890d192991..f40ad589c60 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -17,13 +17,13 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IQuickInputService, IQuickWidget } from 'vs/platform/quickinput/common/quickInput'; import { editorBackground, inputBackground, quickInputBackground, quickInputForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IChatWidgetService, IQuickChatService, IQuickChatOpenOptions } from 'vs/workbench/contrib/chat/browser/chat'; -import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { IQuickChatOpenOptions, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class QuickChatService extends Disposable implements IQuickChatService { readonly _serviceBrand: undefined; @@ -45,7 +45,7 @@ export class QuickChatService extends Disposable implements IQuickChatService { } get enabled(): boolean { - return this.chatService.getProviderInfos().length > 0; + return !!this.chatService.isEnabled(ChatAgentLocation.Panel); } get focused(): boolean { @@ -56,13 +56,13 @@ export class QuickChatService extends Disposable implements IQuickChatService { return dom.isAncestorOfActiveElement(widget); } - toggle(providerId?: string, options?: IQuickChatOpenOptions): void { + toggle(options?: IQuickChatOpenOptions): void { // If the input is already shown, hide it. This provides a toggle behavior of the quick // pick. This should not happen when there is a query. if (this.focused && !options?.query) { this.close(); } else { - this.open(providerId, options); + this.open(options); // If this is a partial query, the value should be cleared when closed as otherwise it // would remain for the next time the quick chat is opened in any context. if (options?.isPartialQuery) { @@ -74,7 +74,7 @@ export class QuickChatService extends Disposable implements IQuickChatService { } } - open(providerId?: string, options?: IQuickChatOpenOptions): void { + open(options?: IQuickChatOpenOptions): void { if (this._input) { if (this._currentChat && options?.query) { this._currentChat.focus(); @@ -87,15 +87,6 @@ export class QuickChatService extends Disposable implements IQuickChatService { return this.focus(); } - // Check if any providers are available. If not, show nothing - // This shouldn't be needed because of the precondition, but just in case - const providerInfo = providerId - ? this.chatService.getProviderInfos().find(info => info.id === providerId) - : this.chatService.getProviderInfos()[0]; - if (!providerInfo) { - return; - } - const disposableStore = new DisposableStore(); this._input = this.quickInputService.createQuickWidget(); @@ -108,9 +99,7 @@ export class QuickChatService extends Disposable implements IQuickChatService { this._input.show(); if (!this._currentChat) { - this._currentChat = this.instantiationService.createInstance(QuickChat, { - providerId: providerInfo.id, - }); + this._currentChat = this.instantiationService.createInstance(QuickChat); // show needs to come after the quickpick is shown this._currentChat.render(this._container); @@ -156,16 +145,15 @@ class QuickChat extends Disposable { private sash!: Sash; private model: ChatModel | undefined; private _currentQuery: string | undefined; - private maintainScrollTimer: MutableDisposable = this._register(new MutableDisposable()); + private readonly maintainScrollTimer: MutableDisposable = this._register(new MutableDisposable()); private _deferUpdatingDynamicLayout: boolean = false; constructor( - private readonly _options: IChatViewOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IChatService private readonly chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - @ILayoutService private readonly layoutService: ILayoutService + @ILayoutService private readonly layoutService: ILayoutService, + @IViewsService private readonly viewsService: IViewsService, ) { super(); } @@ -218,14 +206,15 @@ class QuickChat extends Disposable { render(parent: HTMLElement): void { if (this.widget) { + // NOTE: if this changes, we need to make sure disposables in this function are tracked differently. throw new Error('Cannot render quick chat twice'); } - const scopedInstantiationService = this.instantiationService.createChild( + const scopedInstantiationService = this._register(this.instantiationService.createChild( new ServiceCollection([ IContextKeyService, this._register(this.contextKeyService.createScoped(parent)) ]) - ); + )); this.widget = this._register( scopedInstantiationService.createInstance( ChatWidget, @@ -283,23 +272,41 @@ class QuickChat extends Disposable { })); } - async acceptInput(): Promise { + async acceptInput() { return this.widget.acceptInput(); } async openChatView(): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId); + const widget = await showChatView(this.viewsService); if (!widget?.viewModel || !this.model) { return; } for (const request of this.model.getRequests()) { if (request.response?.response.value || request.response?.result) { + + + const message: IChatProgress[] = []; + for (const item of request.response.response.value) { + if (item.kind === 'textEditGroup') { + for (const group of item.edits) { + message.push({ + kind: 'textEdit', + edits: group, + uri: item.uri + }); + } + } else { + message.push(item); + } + } + this.chatService.addCompleteRequest(widget.viewModel.sessionId, request.message as IParsedChatRequest, request.variableData, + request.attempt, { - message: request.response.response.value, + message, result: request.response.result, followups: request.response.followups }); @@ -325,7 +332,7 @@ class QuickChat extends Disposable { } private updateModel(): void { - this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None); + this.model ??= this.chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None); if (!this.model) { throw new Error('Could not start chat session'); } diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts new file mode 100644 index 00000000000..e1f8491a49f --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.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 { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IChatWidgetService, IChatWidget, ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export class ChatResponseAccessibleView implements IAccessibleViewImplentation { + readonly priority = 100; + readonly name = 'panelChat'; + readonly type = AccessibleViewType.View; + readonly when = CONTEXT_IN_CHAT_SESSION; + private _initialRender = true; + getProvider(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + const codeEditorService = accessor.get(ICodeEditorService); + + const widget = widgetService.lastFocusedWidget; + if (!widget) { + return; + } + const chatInputFocused = this._initialRender && !!codeEditorService.getFocusedCodeEditor() || false; + if (this._initialRender && chatInputFocused) { + widget.focusLastMessage(); + this._initialRender = false; + } + + if (!widget) { + return; + } + + const verifiedWidget: IChatWidget = widget; + const focusedItem = verifiedWidget.getFocus(); + + if (!focusedItem) { + return; + } + + return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem, chatInputFocused); + } +} + +class ChatResponseAccessibleProvider extends Disposable implements IAccessibleViewContentProvider { + private _focusedItem: ChatTreeItem; + constructor( + private readonly _widget: IChatWidget, + item: ChatTreeItem, + private readonly _chatInputFocused: boolean + ) { + super(); + this._focusedItem = item; + } + + readonly id = AccessibleViewProviderId.Chat; + readonly verbositySettingKey = AccessibilityVerbositySettingId.Chat; + readonly options = { type: AccessibleViewType.View }; + + provideContent(): string { + return this._getContent(this._focusedItem); + } + + private _getContent(item: ChatTreeItem): string { + const isWelcome = item instanceof ChatWelcomeMessageModel; + let responseContent = isResponseVM(item) ? item.response.toString() : ''; + if (isWelcome) { + const welcomeReplyContents = []; + for (const content of item.content) { + if (Array.isArray(content)) { + welcomeReplyContents.push(...content.map(m => m.message)); + } else { + welcomeReplyContents.push((content as IMarkdownString).value); + } + } + responseContent = welcomeReplyContents.join('\n'); + } + if (!responseContent && 'errorDetails' in item && item.errorDetails) { + responseContent = item.errorDetails.message; + } + return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); + } + + onClose(): void { + this._widget.reveal(this._focusedItem); + if (this._chatInputFocused) { + this._widget.focusInput(); + } else { + this._widget.focus(this._focusedItem); + } + } + + provideNextContent(): string | undefined { + const next = this._widget.getSibling(this._focusedItem, 'next'); + if (next) { + this._focusedItem = next; + return this._getContent(next); + } + return; + } + + providePreviousContent(): string | undefined { + const previous = this._widget.getSibling(this._focusedItem, 'previous'); + if (previous) { + this._focusedItem = previous; + return this._getContent(previous); + } + return; + } +} + diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 9547c45ba36..72beab2c568 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -3,17 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { basename } from 'vs/base/common/path'; import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { URI } from 'vs/base/common/uri'; +import { Location } from 'vs/editor/common/languages'; +import { IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatDynamicVariableModel } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestDynamicVariablePart, ChatRequestVariablePart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; interface IChatData { data: IChatVariableData; @@ -26,11 +32,12 @@ export class ChatVariablesService implements IChatVariablesService { private _resolver = new Map(); constructor( - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IViewsService private readonly viewsService: IViewsService, ) { } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { let resolvedVariables: IChatRequestVariableEntry[] = []; const jobs: Promise[] = []; @@ -47,34 +54,64 @@ export class ChatVariablesService implements IChatVariablesService { } progress(item); }; - jobs.push(data.resolver(prompt.text, part.variableArg, model, variableProgressCallback, token).then(values => { - resolvedVariables[i] = { name: part.variableName, range: part.range, values: values ?? [], references }; + jobs.push(data.resolver(prompt.text, part.variableArg, model, variableProgressCallback, token).then(value => { + if (value) { + resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: part.variableName, range: part.range, value, references, fullName: data.data.fullName, icon: data.data.icon }; + } }).catch(onUnexpectedExternalError)); } } else if (part instanceof ChatRequestDynamicVariablePart) { - resolvedVariables[i] = { name: part.referenceText, range: part.range, values: part.data }; + resolvedVariables[i] = { id: part.id, name: part.referenceText, range: part.range, value: part.data }; + } + }); + + const resolvedAttachedContext: IChatRequestVariableEntry[] = []; + attachedContextVariables + ?.forEach((attachment, i) => { + const data = this._resolver.get(attachment.name?.toLowerCase()); + if (data) { + const references: IChatContentReference[] = []; + const variableProgressCallback = (item: IChatVariableResolverProgress) => { + if (item.kind === 'reference') { + references.push(item); + return; + } + progress(item); + }; + jobs.push(data.resolver(prompt.text, '', model, variableProgressCallback, token).then(value => { + if (value) { + resolvedAttachedContext[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, fullName: attachment.fullName, range: attachment.range, value, references, icon: attachment.icon }; + } + }).catch(onUnexpectedExternalError)); + } else if (attachment.isDynamic || attachment.isTool) { + resolvedAttachedContext[i] = { ...attachment }; } }); await Promise.allSettled(jobs); - resolvedVariables = coalesce(resolvedVariables); + // Make array not sparse + resolvedVariables = coalesce(resolvedVariables); // "reverse", high index first so that replacement is simple resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); + // resolvedAttachedContext is a sparse array + resolvedVariables.push(...coalesce(resolvedAttachedContext)); + + return { variables: resolvedVariables, }; } - async resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { const data = this._resolver.get(variableName.toLowerCase()); if (!data) { - return Promise.resolve([]); + return undefined; } - return (await data.resolver(promptText, undefined, model, progress, token)) ?? []; + return (await data.resolver(promptText, undefined, model, progress, token)); } hasVariable(name: string): boolean { @@ -85,9 +122,12 @@ export class ChatVariablesService implements IChatVariablesService { return this._resolver.get(name.toLowerCase())?.data; } - getVariables(): Iterable> { + getVariables(location: ChatAgentLocation): Iterable> { const all = Iterable.map(this._resolver.values(), data => data.data); - return Iterable.filter(all, data => !data.hidden); + return Iterable.filter(all, data => { + // TODO@jrieken this is improper and should be know from the variable registeration data + return location !== ChatAgentLocation.Editor || !new Set(['selection', 'editor']).has(data.name); + }); } getDynamicVariables(sessionId: string): ReadonlyArray { @@ -118,4 +158,30 @@ export class ChatVariablesService implements IChatVariablesService { this._resolver.delete(key); }); } + + async attachContext(name: string, value: string | URI | Location, location: ChatAgentLocation) { + if (location !== ChatAgentLocation.Panel) { + return; + } + + const widget = await showChatView(this.viewsService); + if (!widget || !widget.viewModel) { + return; + } + + const key = name.toLowerCase(); + if (key === 'file' && typeof value !== 'string') { + const uri = URI.isUri(value) ? value : value.uri; + const range = 'range' in value ? value.range : undefined; + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isFile: true, isDynamic: true }); + return; + } + + const resolved = this._resolver.get(key); + if (!resolved) { + return; + } + + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { ...resolved.data, value }); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 1a0a353ac1d..0cb92990dff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -8,6 +8,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -21,35 +22,29 @@ import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/vie import { Memento } from 'vs/workbench/common/memento'; import { SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { IChatViewPane } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; +import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; - -export interface IChatViewOptions { - readonly providerId: string; -} +import { IChatViewTitleActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; interface IViewPaneState extends IChatViewState { sessionId?: string; } export const CHAT_SIDEBAR_PANEL_ID = 'workbench.panel.chatSidebar'; -export class ChatViewPane extends ViewPane implements IChatViewPane { - static ID = 'workbench.panel.chat.view'; - +export class ChatViewPane extends ViewPane { private _widget!: ChatWidget; get widget(): ChatWidget { return this._widget; } - private modelDisposables = this._register(new DisposableStore()); + private readonly modelDisposables = this._register(new DisposableStore()); private memento: Memento; private readonly viewState: IViewPaneState; private didProviderRegistrationFail = false; private didUnregisterProvider = false; constructor( - private readonly chatViewOptions: IChatViewOptions, options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @@ -60,58 +55,66 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IHoverService hoverService: IHoverService, @IStorageService private readonly storageService: IStorageService, @IChatService private readonly chatService: IChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, @ILogService private readonly logService: ILogService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); // View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento. - this.memento = new Memento('interactive-session-view-' + this.chatViewOptions.providerId, this.storageService); + this.memento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID, this.storageService); this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState; - this._register(this.chatService.onDidRegisterProvider(({ providerId }) => { - if (providerId === this.chatViewOptions.providerId && !this._widget?.viewModel) { - const sessionId = this.getSessionId(); - const model = sessionId ? this.chatService.getOrRestoreSession(sessionId) : undefined; + this._register(this.chatAgentService.onDidChangeAgents(() => { + if (this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { + if (!this._widget?.viewModel) { + const sessionId = this.getSessionId(); + const model = sessionId ? this.chatService.getOrRestoreSession(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` - // so it should fire onDidChangeViewWelcomeState. - try { - this._widget.setVisible(false); - this.updateModel(model); - this.didProviderRegistrationFail = false; - this.didUnregisterProvider = false; - this._onDidChangeViewWelcomeState.fire(); - } finally { - this.widget.setVisible(true); + // 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` + // so it should fire onDidChangeViewWelcomeState. + try { + this._widget.setVisible(false); + this.updateModel(model); + this.didProviderRegistrationFail = false; + this.didUnregisterProvider = false; + this._onDidChangeViewWelcomeState.fire(); + } finally { + this.widget.setVisible(true); + } } - } - })); - this._register(this.chatService.onDidUnregisterProvider(({ providerId }) => { - if (providerId === this.chatViewOptions.providerId) { + } else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) { + // Model is initialized, and the default agent disappeared, so show welcome view this.didUnregisterProvider = true; this._onDidChangeViewWelcomeState.fire(); } })); } - private updateModel(model?: IChatModel | undefined, viewState?: IViewPaneState): void { + override getActionsContext(): IChatViewTitleActionContext { + return { + chatView: this + }; + } + + private updateModel(model?: IChatModel | undefined): void { this.modelDisposables.clear(); model = model ?? (this.chatService.transferredSessionData?.sessionId ? this.chatService.getOrRestoreSession(this.chatService.transferredSessionData.sessionId) - : this.chatService.startSession(this.chatViewOptions.providerId, CancellationToken.None)); + : this.chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); if (!model) { throw new Error('Could not start chat session'); } - this._widget.setModel(model, { ...(viewState ?? this.viewState) }); + this._widget.setModel(model, { ...this.viewState }); this.viewState.sessionId = model.sessionId; } override shouldShowWelcome(): boolean { - const noPersistedSessions = !this.chatService.hasSessions(this.chatViewOptions.providerId); + const noPersistedSessions = !this.chatService.hasSessions(); return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail); } @@ -130,8 +133,8 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { try { super.renderBody(parent); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + const locationBasedColors = this.getLocationBasedColors(); this._widget = this._register(scopedInstantiationService.createInstance( ChatWidget, ChatAgentLocation.Panel, @@ -139,8 +142,8 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { { supportsFileReferences: true }, { listForeground: SIDE_BAR_FOREGROUND, - listBackground: this.getBackgroundColor(), - inputEditorBackground: this.getBackgroundColor(), + listBackground: locationBasedColors.background, + inputEditorBackground: locationBasedColors.background, resultEditorBackground: editorBackground })); this._register(this.onDidChangeBodyVisibility(visible => { @@ -153,7 +156,7 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { // Render the welcome view if this session gets disposed at any point, // including if the provider registration fails const disposeListener = sessionId ? this._register(this.chatService.onDidDisposeSession((e) => { - if (e.reason === 'initializationFailed' && e.providerId === this.chatViewOptions.providerId) { + if (e.reason === 'initializationFailed') { this.didProviderRegistrationFail = true; disposeListener?.dispose(); this._onDidChangeViewWelcomeState.fire(); @@ -172,11 +175,14 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { this._widget.acceptInput(query); } - async clear(): Promise { + private clear(): void { if (this.widget.viewModel) { this.chatService.clearSession(this.widget.viewModel.sessionId); } - this.updateModel(undefined, { ...this.viewState, inputValue: undefined }); + + // Grab the widget's latest view state because it will be loaded back into the widget + this.updateViewState(); + this.updateModel(undefined); } loadSession(sessionId: string): void { @@ -208,12 +214,16 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { // TODO multiple chat views will overwrite each other this._widget.saveState(); - const widgetViewState = this._widget.getViewState(); - this.viewState.inputValue = widgetViewState.inputValue; - this.viewState.inputState = widgetViewState.inputState; + this.updateViewState(); this.memento.saveMemento(); } super.saveState(); } + + private updateViewState(): void { + const widgetViewState = this._widget.getViewState(); + this.viewState.inputValue = widgetViewState.inputValue; + this.viewState.inputState = widgetViewState.inputState; + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index eaa091f41fd..7b08b5f2aed 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -10,10 +10,11 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { isEqual } from 'vs/base/common/resources'; +import { extUri, isEqual } from 'vs/base/common/resources'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/chat'; +import 'vs/css!./media/chatAgentHover'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { MenuId } from 'vs/platform/actions/common/actions'; @@ -25,23 +26,20 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityProvider } from 'vs/workbench/contrib/chat/browser/chatAccessibilityProvider'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_QUICK_CHAT, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatModelInitState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, formatChatQuestion } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { IChatFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatLocationData, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; const $ = dom.$; @@ -76,12 +74,20 @@ export interface IChatWidgetContrib extends IDisposable { setInputState?(s: any): void; } +export interface IChatWidgetLocationOptions { + location: ChatAgentLocation; + resolveData?(): IChatLocationData | undefined; +} + export class ChatWidget extends Disposable implements IChatWidget { public static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = []; private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>()); public readonly onDidSubmitAgent = this._onDidSubmitAgent.event; + private _onDidChangeAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>()); + readonly onDidChangeAgent = this._onDidChangeAgent.event; + private _onDidFocus = this._register(new Emitter()); readonly onDidFocus = this._onDidFocus.event; @@ -97,13 +103,25 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidAcceptInput = this._register(new Emitter()); readonly onDidAcceptInput = this._onDidAcceptInput.event; + private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>()); + readonly onDidChangeContext = this._onDidChangeContext.event; + + private _onDidHide = this._register(new Emitter()); + readonly onDidHide = this._onDidHide.event; + + private _onDidChangeParsedInput = this._register(new Emitter()); + readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; + + private readonly _onWillMaybeChangeHeight = new Emitter(); + readonly onWillMaybeChangeHeight: Event = this._onWillMaybeChangeHeight.event; + private _onDidChangeHeight = this._register(new Emitter()); readonly onDidChangeHeight = this._onDidChangeHeight.event; private readonly _onDidChangeContentHeight = new Emitter(); readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; - private contribs: IChatWidgetContrib[] = []; + private contribs: ReadonlyArray = []; private tree!: WorkbenchObjectTree; private renderer!: ChatListItemRenderer; @@ -127,7 +145,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private previousTreeScrollHeight: number = 0; - private viewModelDisposables = this._register(new DisposableStore()); + private readonly viewModelDisposables = this._register(new DisposableStore()); private _viewModel: ChatViewModel | undefined; private set viewModel(viewModel: ChatViewModel | undefined) { if (this._viewModel === viewModel) { @@ -159,8 +177,18 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.parsedChatRequest; } + get scopedContextKeyService(): IContextKeyService { + return this.contextKeyService; + } + + private readonly _location: IChatWidgetLocationOptions; + + get location() { + return this._location.location; + } + constructor( - readonly location: ChatAgentLocation, + location: ChatAgentLocation | IChatWidgetLocationOptions, readonly viewContext: IChatWidgetViewContext, private readonly viewOptions: IChatWidgetViewOptions, private readonly styles: IChatWidgetStyles, @@ -177,8 +205,16 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, ) { super(); + + if (typeof location === 'object') { + this._location = location; + } else { + this._location = { location }; + } + CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); - CONTEXT_CHAT_LOCATION.bindTo(contextKeyService).set(location); + CONTEXT_CHAT_LOCATION.bindTo(contextKeyService).set(this._location.location); + CONTEXT_IN_QUICK_CHAT.bindTo(contextKeyService).set('resource' in viewContext); this.agentInInput = CONTEXT_CHAT_INPUT_HAS_AGENT.bindTo(contextKeyService); this.requestInProgress = CONTEXT_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); @@ -187,11 +223,12 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { - if (input.resource.scheme !== Schemas.vscodeChatCodeBlock) { + const resource = input.resource; + if (resource.scheme !== Schemas.vscodeChatCodeBlock) { return null; } - const responseId = input.resource.path.split('/').at(1); + const responseId = resource.path.split('/').at(1); if (!responseId) { return null; } @@ -201,19 +238,21 @@ export class ChatWidget extends Disposable implements IChatWidget { return null; } + // TODO: needs to reveal the chat view + this.reveal(item); await timeout(0); // wait for list to actually render for (const editor of this.renderer.editorsInUse() ?? []) { - if (editor.uri?.toString() === input.resource.toString()) { + if (extUri.isEqual(editor.uri, resource, true)) { const inner = editor.editor; if (input.options?.selection) { inner.setSelection({ startLineNumber: input.options.selection.startLineNumber, startColumn: input.options.selection.startColumn, - endLineNumber: input.options.selection.startLineNumber ?? input.options.selection.endLineNumber, - endColumn: input.options.selection.startColumn ?? input.options.selection.endColumn + endLineNumber: input.options.selection.endLineNumber ?? input.options.selection.startLineNumber, + endColumn: input.options.selection.endColumn ?? input.options.selection.startColumn }); } return inner; @@ -225,7 +264,9 @@ export class ChatWidget extends Disposable implements IChatWidget { private _lastSelectedAgent: IChatAgentData | undefined; set lastSelectedAgent(agent: IChatAgentData | undefined) { + this.parsedChatRequest = undefined; this._lastSelectedAgent = agent; + this._onDidChangeParsedInput.fire(); } get lastSelectedAgent(): IChatAgentData | undefined { @@ -236,10 +277,6 @@ export class ChatWidget extends Disposable implements IChatWidget { return !!this.viewOptions.supportsFileReferences; } - get providerId(): string { - return this.viewModel?.providerId || ''; - } - get input(): ChatInputPart { return this.inputPart; } @@ -253,25 +290,26 @@ export class ChatWidget extends Disposable implements IChatWidget { } get contentHeight(): number { - return dom.getTotalHeight(this.inputPart.element) + this.tree.contentHeight; + return this.inputPart.contentHeight + this.tree.contentHeight; } render(parent: HTMLElement): void { const viewId = 'viewId' in this.viewContext ? this.viewContext.viewId : undefined; this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground)); const renderInputOnTop = this.viewOptions.renderInputOnTop ?? false; + const renderFollowups = this.viewOptions.renderFollowups ?? !renderInputOnTop; const renderStyle = this.viewOptions.renderStyle; this.container = dom.append(parent, $('.interactive-session')); if (renderInputOnTop) { - this.createInput(this.container, { renderFollowups: false, renderStyle }); + this.createInput(this.container, { renderFollowups, renderStyle }); this.listContainer = dom.append(this.container, $(`.interactive-list`)); } else { this.listContainer = dom.append(this.container, $(`.interactive-list`)); - this.createInput(this.container, { renderFollowups: true, renderStyle }); + this.createInput(this.container, { renderFollowups, renderStyle }); } - this.createList(this.listContainer, { renderStyle, editableCodeBlock: this.viewOptions.editableCodeBlocks }); + this.createList(this.listContainer, { ...this.viewOptions.rendererOptions, renderStyle }); this._register(this.editorOptions.onDidChange(() => this.onDidStyleChange())); this.onDidStyleChange(); @@ -304,7 +342,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.inputPart.hasFocus(); } - moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void { + getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined { if (!isResponseVM(item)) { return; } @@ -321,7 +359,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) { return; } - this.focus(responseItems[indexToFocus]); + return responseItems[indexToFocus]; } clear(): void { @@ -334,14 +372,16 @@ export class ChatWidget extends Disposable implements IChatWidget { private onDidChangeItems(skipDynamicLayout?: boolean) { if (this.tree && this._visible) { const treeItems = (this.viewModel?.getItems() ?? []) - .map(item => { - return >{ + .map((item): ITreeElement => { + return { element: item, collapsed: false, collapsible: false }; }); + this._onWillMaybeChangeHeight.fire(); + this.tree.setChildren(null, treeItems, { diffIdentityProvider: { getId: (element) => { @@ -354,7 +394,10 @@ export class ChatWidget extends Disposable implements IChatWidget { // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. `${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` + // Re-render once content references are loaded - (isResponseVM(element) ? `_${element.contentReferences.length}` : ''); + (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + + // Rerender request if we got new content references in the response + // since this may change how we render the corresponding attachments in the request + (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); }, } }); @@ -383,9 +426,11 @@ export class ChatWidget extends Disposable implements IChatWidget { } setVisible(visible: boolean): void { + const wasVisible = this._visible; this._visible = visible; this.visibleChangeCount++; this.renderer.setVisible(visible); + this.input.setVisible(visible); if (visible) { this._register(disposableTimeout(() => { @@ -395,11 +440,13 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(true); } }, 0)); + } else if (wasVisible) { + this._onDidHide.fire(); } } private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const scopedInstantiationService = this._register(this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])))); const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200); const rendererDelegate: IChatRendererDelegate = { getListLength: () => this.tree.getNode(null).visibleChildrenCount, @@ -424,6 +471,12 @@ export class ChatWidget extends Disposable implements IChatWidget { // is this used anymore? this.acceptInput(item.message); })); + this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(item => { + const request = this.chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); + if (request) { + this.chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: this.location }).catch(e => this.logService.error('FAILED to rerun request', e)); + } + })); this.tree = >scopedInstantiationService.createInstance( WorkbenchObjectTree, @@ -434,6 +487,7 @@ export class ChatWidget extends Disposable implements IChatWidget { { identityProvider: { getId: (e: ChatTreeItem) => e.id }, horizontalScrolling: false, + alwaysConsumeMouseWheel: false, supportDynamicHeights: true, hideTwistiesOfChildlessElements: true, accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider), @@ -506,26 +560,29 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onDidChangeContentHeight.fire(); } - private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' }): void { + private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' | 'minimal' }): void { this.inputPart = this._register(this.instantiationService.createInstance(ChatInputPart, this.location, { renderFollowups: options?.renderFollowups ?? true, - renderStyle: options?.renderStyle, + renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, - } + }, + () => this.collectInputState() )); this.inputPart.render(container, '', this); this._register(this.inputPart.onDidLoadInputState(state => { this.contribs.forEach(c => { - if (c.setInputState && typeof state === 'object' && state?.[c.id]) { - c.setInputState(state[c.id]); + if (c.setInputState) { + const contribState = (typeof state === 'object' && state?.[c.id]) ?? {}; + c.setInputState(contribState); } }); })); this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire())); + this._register(this.inputPart.onDidChangeContext((e) => this._onDidChangeContext.fire(e))); this._register(this.inputPart.onDidAcceptFollowup(e => { if (!this.viewModel) { return; @@ -533,7 +590,13 @@ export class ChatWidget extends Disposable implements IChatWidget { let msg = ''; if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { - msg = `${chatAgentLeader}${e.followup.agentId} `; + const agent = this.chatAgentService.getAgent(e.followup.agentId); + if (!agent) { + return; + } + + this.lastSelectedAgent = agent; + msg = `${chatAgentLeader}${agent.name} `; if (e.followup.subCommand) { msg += `${chatSubcommandLeader}${e.followup.subCommand} `; } @@ -551,10 +614,10 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.chatService.notifyUserAction({ - providerId: this.viewModel.providerId, sessionId: this.viewModel.sessionId, requestId: e.response.requestId, agentId: e.response.agent?.id, + command: e.response.slashCommand?.name, result: e.response.result, action: { kind: 'followUp', @@ -568,12 +631,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } this._onDidChangeContentHeight.fire(); })); - this._register(this.inputEditor.onDidChangeModelContent(() => this.updateImplicitContextKinds())); - this._register(this.chatAgentService.onDidChangeAgents(() => { - if (this.viewModel) { - this.updateImplicitContextKinds(); - } - })); + this._register(this.inputEditor.onDidChangeModelContent(() => this.parsedChatRequest = undefined)); + this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); } private onDidStyleChange(): void { @@ -582,23 +641,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); } - private updateImplicitContextKinds() { - if (!this.viewModel) { - return; - } - this.parsedChatRequest = undefined; - const agentAndSubcommand = extractAgentAndCommand(this.parsedInput); - const currentAgent = agentAndSubcommand.agentPart?.agent ?? this.chatAgentService.getDefaultAgent(this.location); - const implicitVariables = agentAndSubcommand.commandPart ? - agentAndSubcommand.commandPart.command.defaultImplicitVariables : - currentAgent?.defaultImplicitVariables; - this.inputPart.setImplicitContextKinds(implicitVariables ?? []); - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - } - setModel(model: IChatModel, viewState: IChatViewState): void { if (!this.container) { throw new Error('Call render() before setModel()'); @@ -616,7 +658,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.requestInProgress.set(this.viewModel.requestInProgress); this.onDidChangeItems(); - if (events.some(e => e?.kind === 'addRequest')) { + if (events.some(e => e?.kind === 'addRequest') && this.visible) { revealLastElement(this.tree); this.focusInput(); } @@ -629,19 +671,23 @@ export class ChatWidget extends Disposable implements IChatWidget { this.viewModel = undefined; this.onDidChangeItems(); })); - this.inputPart.setState(model.providerId, viewState.inputValue); + this.inputPart.initForNewChatModel(viewState.inputValue, viewState.inputState ?? this.collectInputState()); this.contribs.forEach(c => { if (c.setInputState && viewState.inputState?.[c.id]) { c.setInputState(viewState.inputState?.[c.id]); } }); + this.viewModelDisposables.add(model.onDidChange((e) => { + if (e.kind === 'setAgent') { + this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command }); + } + })); if (this.tree) { this.onDidChangeItems(); revealLastElement(this.tree); } - this.updateImplicitContextKinds(); } getFocus(): ChatTreeItem | undefined { @@ -676,15 +722,19 @@ export class ChatWidget extends Disposable implements IChatWidget { } setInput(value = ''): void { - this.inputPart.setValue(value); + this.inputPart.setValue(value, false); } getInput(): string { return this.inputPart.inputEditor.getValue(); } - async acceptInput(query?: string): Promise { - this._acceptInput(query ? { query } : undefined); + logInputHistory(): void { + this.inputPart.logInputHistory(); + } + + async acceptInput(query?: string): Promise { + return this._acceptInput(query ? { query } : undefined); } async acceptInputWithPrefix(prefix: string): Promise { @@ -701,7 +751,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } - private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { + private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { if (this.viewModel) { this._onDidAcceptInput.fire(); @@ -711,19 +761,36 @@ export class ChatWidget extends Disposable implements IChatWidget { 'query' in opts ? opts.query : `${opts.prefix} ${editorValue}`; const isUserQuery = !opts || 'prefix' in opts; - const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled, this.location, { selectedAgent: this._lastSelectedAgent }); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { + location: this.location, + locationData: this._location.resolveData?.(), + parserContext: { selectedAgent: this._lastSelectedAgent }, + attachedContext: [...this.inputPart.attachedContext.values()] + }); if (result) { - const inputState = this.collectInputState(); - this.inputPart.acceptInput(isUserQuery ? input : undefined, isUserQuery ? inputState : undefined); + this.inputPart.acceptInput(isUserQuery); this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); - result.responseCompletePromise.then(async () => { + result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; this.chatAccessibilityService.acceptResponse(lastResponse, requestId); + if (lastResponse?.result?.nextQuestion) { + const { prompt, participant, command } = lastResponse.result.nextQuestion; + const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command); + if (question) { + this.input.setValue(question, false); + } + } }); + return result.responseCreatedPromise; } } + return undefined; + } + + setContext(overwrite: boolean, ...contentReferences: IChatRequestVariableEntry[]) { + this.inputPart.attachContext(overwrite, ...contentReferences); } getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { @@ -884,11 +951,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } getViewState(): IChatViewState { - this.inputPart.saveState(); return { inputValue: this.getInput(), inputState: this.collectInputState() }; } - - } export class ChatWidgetService implements IChatWidgetService { @@ -902,10 +966,7 @@ export class ChatWidgetService implements IChatWidgetService { return this._lastFocusedWidget; } - constructor( - @IViewsService private readonly viewsService: IViewsService, - @IChatContributionService private readonly chatContributionService: IChatContributionService, - ) { } + constructor() { } getWidgetByInputUri(uri: URI): ChatWidget | undefined { return this._widgets.find(w => isEqual(w.inputUri, uri)); @@ -915,13 +976,6 @@ export class ChatWidgetService implements IChatWidgetService { return this._widgets.find(w => w.viewModel?.sessionId === sessionId); } - async revealViewForProvider(providerId: string): Promise { - const viewId = this.chatContributionService.getViewIdForProvider(providerId); - const view = await this.viewsService.openView(viewId); - - return view?.widget; - } - private setLastFocusedWidget(widget: ChatWidget | undefined): void { if (widget === this._lastFocusedWidget) { return; diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css index 5c778eb2601..89596f89748 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css @@ -8,15 +8,30 @@ position: relative; } -.interactive-result-code-block .monaco-toolbar { +.interactive-result-code-block .interactive-result-code-block-toolbar { display: none; +} + +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-action-bar, +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-toolbar { position: absolute; top: -13px; - right: 10px; height: 26px; + line-height: 26px; background-color: var(--vscode-interactive-result-editor-background-color, var(--vscode-editor-background)); border: 1px solid var(--vscode-chat-requestBorder); z-index: 100; + max-width: 70%; + text-overflow: ellipsis; + overflow: hidden; +} + +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-action-bar { + left: 0px +} + +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-toolbar { + right: 10px; } .interactive-result-code-block .monaco-toolbar .action-item { @@ -29,9 +44,9 @@ margin: 1px; } -.interactive-result-code-block:hover .monaco-toolbar, -.interactive-result-code-block .monaco-toolbar:focus-within, -.interactive-result-code-block.focused .monaco-toolbar { +.interactive-result-code-block:hover .interactive-result-code-block-toolbar, +.interactive-result-code-block .interactive-result-code-block-toolbar:focus-within, +.interactive-result-code-block.focused .interactive-result-code-block-toolbar { display: initial; border-radius: 2px; } @@ -40,7 +55,7 @@ display: initial !important; } -.interactive-result-code-block { +.interactive-item-container .value .rendered-markdown [data-code] { margin: 16px 0; } @@ -111,3 +126,38 @@ .interactive-result-code-block .interactive-result-vulns-header .monaco-text-button:focus-visible { outline: 1px solid var(--vscode-focusBorder); } + +/* compare code block */ + +.interactive-result-code-block.compare.no-diff .message { + display: inherit; +} + +.interactive-result-code-block.compare .message { + display: none; + padding: 6px; +} + + +.interactive-result-code-block.compare .message A { + color: var(--vscode-textLink-foreground); + cursor: pointer; +} + +.interactive-result-code-block.compare .message A > CODE { + color: var(--vscode-textLink-foreground); +} + +.interactive-result-code-block.compare .interactive-result-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 3px; + box-sizing: border-box; + border-bottom: solid 1px var(--vscode-chat-requestBorder); +} + +.interactive-result-code-block.compare.no-diff .interactive-result-header, +.interactive-result-code-block.compare.no-diff .interactive-result-editor { + display: none; +} diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 01985bddc9b..bc5cea824fa 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -6,25 +6,37 @@ import 'vs/css!./codeBlockPart'; import * as dom from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { Button } from 'vs/base/browser/ui/button/button'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { TabFocus } from 'vs/editor/browser/config/tabFocus'; +import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EDITOR_FONT_DEFAULTS, EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IDiffEditorViewModel, ScrollType } from 'vs/editor/common/editorCommon'; +import { TextEdit } from 'vs/editor/common/languages'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; +import { TextModelText } from 'vs/editor/common/model/textModelText'; import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/browser/bracketMatching'; +import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hover'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { ViewportSemanticTokensContribution } from 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import { SmartSelectController } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { WordHighlighterContribution } from 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; @@ -34,17 +46,25 @@ import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CONTEXT_CHAT_EDIT_APPLIED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatResponseModel, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IMarkdownVulnerability } from '../common/annotations'; -import { TabFocus } from 'vs/editor/browser/config/tabFocus'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { FileKind } from 'vs/platform/files/common/files'; +import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; const $ = dom.$; @@ -124,6 +144,11 @@ export class CodeBlockPart extends Disposable { private currentCodeBlockData: ICodeBlockData | undefined; private currentScrollWidth = 0; + private readonly disposableStore = this._register(new DisposableStore()); + private isDisposed = false; + + private resourceContextKey: ResourceContextKey; + constructor( private readonly options: ChatEditorOptions, readonly menuId: MenuId, @@ -138,8 +163,9 @@ export class CodeBlockPart extends Disposable { super(); this.element = $('.interactive-result-code-block'); + this.resourceContextKey = this._register(instantiationService.createInstance(ResourceContextKey)); this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); - const scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); const editorElement = dom.append(this.element, $('.interactive-result-editor')); this.editor = this.createEditor(scopedInstantiationService, editorElement, { ...getSimpleEditorOptions(this.configurationService), @@ -169,7 +195,7 @@ export class CodeBlockPart extends Disposable { const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); const editorScopedService = this.editor.contextKeyService.createScoped(toolbarElement); - const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); + const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService]))); this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { menuOptions: { shouldForwardArgs: true @@ -243,6 +269,11 @@ export class CodeBlockPart extends Disposable { } } + override dispose() { + this.isDisposed = true; + super.dispose(); + } + get uri(): URI | undefined { return this.editor.getModel()?.uri; } @@ -260,7 +291,9 @@ export class CodeBlockPart extends Disposable { BracketMatchingController.ID, SmartSelectController.ID, HoverController.ID, + MessageController.ID, GotoDefinitionAtPositionEditorContribution.ID, + ColorDetector.ID ]) })); } @@ -333,11 +366,15 @@ export class CodeBlockPart extends Disposable { } await this.updateEditor(data); + if (this.isDisposed) { + return; + } this.layout(width); if (editable) { - this._register(this.editor.onDidFocusEditorWidget(() => TabFocus.setTabFocusMode(true))); - this._register(this.editor.onDidBlurEditorWidget(() => TabFocus.setTabFocusMode(false))); + this.disposableStore.clear(); + this.disposableStore.add(this.editor.onDidFocusEditorWidget(() => TabFocus.setTabFocusMode(true))); + this.disposableStore.add(this.editor.onDidBlurEditorWidget(() => TabFocus.setTabFocusMode(false))); } this.editor.updateOptions({ ariaLabel: localize('chat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1), readOnly: !editable }); @@ -380,6 +417,7 @@ export class CodeBlockPart extends Disposable { element: data.element, languageId: textModel.getLanguageId() } satisfies ICodeBlockActionContext; + this.resourceContextKey.set(textModel.uri); } private getVulnerabilitiesLabel(): string { @@ -413,3 +451,468 @@ export class ChatCodeBlockContentProvider extends Disposable implements ITextMod return this._modelService.createModel('', null, resource); } } + +// + +export interface ICodeCompareBlockActionContext { + readonly element: IChatResponseViewModel; + readonly diffEditor: IDiffEditor; + readonly edit: IChatTextEditGroup; +} + +export interface ICodeCompareBlockDiffData { + modified: ITextModel; + original: ITextModel; + originalSha1: string; +} + +export interface ICodeCompareBlockData { + readonly element: ChatTreeItem; + + readonly edit: IChatTextEditGroup; + + readonly diffData: Promise; + + readonly parentContextKeyService?: IContextKeyService; + // readonly hideToolbar?: boolean; +} + + +export class CodeCompareBlockPart extends Disposable { + protected readonly _onDidChangeContentHeight = this._register(new Emitter()); + public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; + + private readonly contextKeyService: IContextKeyService; + private readonly diffEditor: DiffEditorWidget; + private readonly resourceLabel: ResourceLabel; + private readonly toolbar: MenuWorkbenchToolBar; + readonly element: HTMLElement; + private readonly messageElement: HTMLElement; + + private readonly _lastDiffEditorViewModel = this._store.add(new MutableDisposable()); + private currentScrollWidth = 0; + + constructor( + private readonly options: ChatEditorOptions, + readonly menuId: MenuId, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IModelService protected readonly modelService: IModelService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ILabelService private readonly labelService: ILabelService, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super(); + this.element = $('.interactive-result-code-block'); + this.element.classList.add('compare'); + + this.messageElement = dom.append(this.element, $('.message')); + this.messageElement.setAttribute('role', 'status'); + this.messageElement.tabIndex = 0; + + this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); + const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); + const editorHeader = dom.append(this.element, $('.interactive-result-header.show-file-icons')); + const editorElement = dom.append(this.element, $('.interactive-result-editor')); + this.diffEditor = this.createDiffEditor(scopedInstantiationService, editorElement, { + ...getSimpleEditorOptions(this.configurationService), + lineNumbers: 'on', + selectOnLineNumbers: true, + scrollBeyondLastLine: false, + lineDecorationsWidth: 12, + dragAndDrop: false, + padding: { top: defaultCodeblockPadding, bottom: defaultCodeblockPadding }, + mouseWheelZoom: false, + scrollbar: { + vertical: 'hidden', + alwaysConsumeMouseWheel: false + }, + definitionLinkOpensInPeek: false, + gotoLocation: { + multiple: 'goto', + multipleDeclarations: 'goto', + multipleDefinitions: 'goto', + multipleImplementations: 'goto', + }, + ariaLabel: localize('chat.codeBlockHelp', 'Code block'), + overflowWidgetsDomNode, + ...this.getEditorOptionsFromConfig(), + }); + + this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, editorHeader, { supportIcons: true })); + + const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(editorHeader); + const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService]))); + this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, editorHeader, menuId, { + menuOptions: { + shouldForwardArgs: true + } + })); + + this._configureForScreenReader(); + this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader())); + this._register(this.configurationService.onDidChangeConfiguration((e) => { + if (e.affectedKeys.has(AccessibilityVerbositySettingId.Chat)) { + this._configureForScreenReader(); + } + })); + + this._register(this.options.onDidChange(() => { + this.diffEditor.updateOptions(this.getEditorOptionsFromConfig()); + })); + + this._register(this.diffEditor.getModifiedEditor().onDidScrollChange(e => { + this.currentScrollWidth = e.scrollWidth; + })); + this._register(this.diffEditor.onDidContentSizeChange(e => { + if (e.contentHeightChanged) { + this._onDidChangeContentHeight.fire(); + } + })); + this._register(this.diffEditor.getModifiedEditor().onDidBlurEditorWidget(() => { + this.element.classList.remove('focused'); + WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.stopHighlighting(); + this.clearWidgets(); + })); + this._register(this.diffEditor.getModifiedEditor().onDidFocusEditorWidget(() => { + this.element.classList.add('focused'); + WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.restoreViewState(true); + })); + + + // Parent list scrolled + if (delegate.onDidScroll) { + this._register(delegate.onDidScroll(e => { + this.clearWidgets(); + })); + } + } + + get uri(): URI | undefined { + return this.diffEditor.getModifiedEditor().getModel()?.uri; + } + + private createDiffEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): DiffEditorWidget { + const widgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + + WordHighlighterContribution.ID, + ViewportSemanticTokensContribution.ID, + BracketMatchingController.ID, + SmartSelectController.ID, + HoverController.ID, + GotoDefinitionAtPositionEditorContribution.ID, + ]) + }; + + return this._register(instantiationService.createInstance(DiffEditorWidget, parent, { + scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, + renderMarginRevertIcon: false, + diffCodeLens: false, + scrollBeyondLastLine: false, + stickyScroll: { enabled: false }, + originalAriaLabel: localize('original', 'Original'), + modifiedAriaLabel: localize('modified', 'Modified'), + diffAlgorithm: 'advanced', + readOnly: false, + isInEmbeddedEditor: true, + useInlineViewWhenSpaceIsLimited: true, + experimental: { + useTrueInlineView: true, + }, + renderSideBySideInlineBreakpoint: 300, + renderOverviewRuler: false, + compactMode: true, + hideUnchangedRegions: { enabled: true, contextLineCount: 1 }, + renderGutterMenu: false, + lineNumbersMinChars: 1, + ...options + }, { originalEditor: widgetOptions, modifiedEditor: widgetOptions })); + } + + focus(): void { + this.diffEditor.focus(); + } + + private updatePaddingForLayout() { + // scrollWidth = "the width of the content that needs to be scrolled" + // contentWidth = "the width of the area where content is displayed" + const horizontalScrollbarVisible = this.currentScrollWidth > this.diffEditor.getModifiedEditor().getLayoutInfo().contentWidth; + const scrollbarHeight = this.diffEditor.getModifiedEditor().getLayoutInfo().horizontalScrollbarHeight; + const bottomPadding = horizontalScrollbarVisible ? + Math.max(defaultCodeblockPadding - scrollbarHeight, 2) : + defaultCodeblockPadding; + this.diffEditor.updateOptions({ padding: { top: defaultCodeblockPadding, bottom: bottomPadding } }); + } + + private _configureForScreenReader(): void { + const toolbarElt = this.toolbar.getElement(); + if (this.accessibilityService.isScreenReaderOptimized()) { + toolbarElt.style.display = 'block'; + toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('chat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('chat.codeBlock.toolbar', 'Code block toolbar'); + } else { + toolbarElt.style.display = ''; + } + } + + private getEditorOptionsFromConfig(): IEditorOptions { + return { + wordWrap: this.options.configuration.resultEditor.wordWrap, + fontLigatures: this.options.configuration.resultEditor.fontLigatures, + bracketPairColorization: this.options.configuration.resultEditor.bracketPairColorization, + fontFamily: this.options.configuration.resultEditor.fontFamily === 'default' ? + EDITOR_FONT_DEFAULTS.fontFamily : + this.options.configuration.resultEditor.fontFamily, + fontSize: this.options.configuration.resultEditor.fontSize, + fontWeight: this.options.configuration.resultEditor.fontWeight, + lineHeight: this.options.configuration.resultEditor.lineHeight, + }; + } + + layout(width: number): void { + const contentHeight = this.getContentHeight(); + const editorBorder = 2; + const dimension = { width: width - editorBorder, height: contentHeight }; + this.element.style.height = `${dimension.height}px`; + this.element.style.width = `${dimension.width}px`; + this.diffEditor.layout(dimension); + this.updatePaddingForLayout(); + } + + private getContentHeight() { + return this.diffEditor.getContentHeight(); + } + + async render(data: ICodeCompareBlockData, width: number, token: CancellationToken) { + if (data.parentContextKeyService) { + this.contextKeyService.updateParent(data.parentContextKeyService); + } + + if (this.options.configuration.resultEditor.wordWrap === 'on') { + // Initialize the editor with the new proper width so that getContentHeight + // will be computed correctly in the next call to layout() + this.layout(width); + } + + await this.updateEditor(data, token); + + this.layout(width); + this.diffEditor.updateOptions({ ariaLabel: localize('chat.compareCodeBlockLabel', "Code Edits") }); + + this.resourceLabel.element.setFile(data.edit.uri, { + fileKind: FileKind.FILE, + fileDecorations: { colors: true, badges: false } + }); + } + + reset() { + this.clearWidgets(); + } + + private clearWidgets() { + HoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); + HoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover(); + } + + private async updateEditor(data: ICodeCompareBlockData, token: CancellationToken): Promise { + + if (!isResponseVM(data.element)) { + return; + } + + const isEditApplied = Boolean(data.edit.state?.applied ?? 0); + + CONTEXT_CHAT_EDIT_APPLIED.bindTo(this.contextKeyService).set(isEditApplied); + + this.element.classList.toggle('no-diff', isEditApplied); + + if (data.edit.state?.applied) { + + const uriLabel = this.labelService.getUriLabel(data.edit.uri, { relative: true, noPrefix: true }); + + let template: string; + if (data.edit.state.applied === 1) { + template = localize('chat.edits.1', "Made 1 change in [[``{0}``]]", uriLabel); + } else if (data.edit.state.applied < 0) { + template = localize('chat.edits.rejected', "Edits in [[``{0}``]] have been rejected", uriLabel); + } else { + template = localize('chat.edits.N', "Made {0} changes in [[``{1}``]]", data.edit.state.applied, uriLabel); + } + + const message = renderFormattedText(template, { + renderCodeSegments: true, + actionHandler: { + callback: () => { + this.openerService.open(data.edit.uri, { fromUserGesture: true, allowCommands: false }); + }, + disposables: this._store, + } + }); + + dom.reset(this.messageElement, message); + + } + + const diffData = await data.diffData; + if (!diffData) { + return; + } + + if (!isEditApplied) { + const viewModel = this.diffEditor.createViewModel({ + original: diffData.original, + modified: diffData.modified + }); + + await viewModel.waitForDiff(); + + if (token.isCancellationRequested) { + return; + } + + this.diffEditor.setModel(viewModel); + this._lastDiffEditorViewModel.value = viewModel; + + } else { + this.diffEditor.setModel(null); + this._lastDiffEditorViewModel.value = undefined; + } + + this.toolbar.context = { + edit: data.edit, + element: data.element, + diffEditor: this.diffEditor, + } satisfies ICodeCompareBlockActionContext; + } + + clearModel() { + this.diffEditor.setModel(null); + } +} + +export class DefaultChatTextEditor { + + private readonly _sha1 = new DefaultModelSHA1Computer(); + + constructor( + @ITextModelService private readonly modelService: ITextModelService, + @ICodeEditorService private readonly editorService: ICodeEditorService, + @IDialogService private readonly dialogService: IDialogService, + ) { } + + async apply(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup, diffEditor: IDiffEditor | undefined): Promise { + + if (!response.response.value.includes(item)) { + // bogous item + return; + } + + if (item.state?.applied) { + // already applied + return; + } + + if (!diffEditor) { + for (const candidate of this.editorService.listDiffEditors()) { + if (!candidate.getContainerDomNode().isConnected) { + continue; + } + const model = candidate.getModel(); + if (!model || !isEqual(model.original.uri, item.uri) || model.modified.uri.scheme !== Schemas.vscodeChatCodeCompareBlock) { + diffEditor = candidate; + break; + } + } + } + + const edits = diffEditor + ? await this._applyWithDiffEditor(diffEditor, item) + : await this._apply(item); + + response.setEditApplied(item, edits); + } + + private async _applyWithDiffEditor(diffEditor: IDiffEditor, item: IChatTextEditGroup) { + const model = diffEditor.getModel(); + if (!model) { + return 0; + } + + const diff = diffEditor.getDiffComputationResult(); + if (!diff || diff.identical) { + return 0; + } + + + if (!await this._checkSha1(model.original, item)) { + return 0; + } + + const modified = new TextModelText(model.modified); + const edits = diff.changes2.map(i => i.toRangeMapping().toTextEdit(modified).toSingleEditOperation()); + + model.original.pushStackElement(); + model.original.pushEditOperations(null, edits, () => null); + model.original.pushStackElement(); + + return edits.length; + } + + private async _apply(item: IChatTextEditGroup) { + const ref = await this.modelService.createModelReference(item.uri); + try { + + if (!await this._checkSha1(ref.object.textEditorModel, item)) { + return 0; + } + + ref.object.textEditorModel.pushStackElement(); + let total = 0; + for (const group of item.edits) { + const edits = group.map(TextEdit.asEditOperation); + ref.object.textEditorModel.pushEditOperations(null, edits, () => null); + total += edits.length; + } + ref.object.textEditorModel.pushStackElement(); + return total; + + } finally { + ref.dispose(); + } + } + + private async _checkSha1(model: ITextModel, item: IChatTextEditGroup) { + if (item.state?.sha1 && this._sha1.computeSHA1(model) && this._sha1.computeSHA1(model) !== item.state.sha1) { + const result = await this.dialogService.confirm({ + message: localize('interactive.compare.apply.confirm', "The original file has been modified."), + detail: localize('interactive.compare.apply.confirm.detail', "Do you want to apply the changes anyway?"), + }); + + if (!result.confirmed) { + return false; + } + } + return true; + } + + discard(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup) { + if (!response.response.value.includes(item)) { + // bogous item + return; + } + + if (item.state?.applied) { + // already applied + return; + } + + response.setEditApplied(item, -1); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts new file mode 100644 index 00000000000..c19bc95305f --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; + +export class ChatContextAttachments extends Disposable implements IChatWidgetContrib { + + private _attachedContext = new Set(); + + public static readonly ID = 'chatContextAttachments'; + + get id() { + return ChatContextAttachments.ID; + } + + constructor(readonly widget: IChatWidget) { + super(); + + this._register(this.widget.onDidChangeContext((e) => { + if (e.removed) { + this._removeContext(e.removed); + } + })); + + this._register(this.widget.onDidSubmitAgent(() => { + this._clearAttachedContext(); + })); + } + + getInputState(): IChatRequestVariableEntry[] { + return [...this._attachedContext.values()]; + } + + setInputState(s: any): void { + if (!Array.isArray(s)) { + s = []; + } + + this._attachedContext.clear(); + for (const attachment of s) { + this._attachedContext.add(attachment); + } + + this.widget.setContext(true, ...s); + } + + getContext() { + return new Set([...this._attachedContext.values()].map((v) => v.id)); + } + + setContext(overwrite: boolean, ...attachments: IChatRequestVariableEntry[]) { + if (overwrite) { + this._attachedContext.clear(); + } + for (const attachment of attachments) { + this._attachedContext.add(attachment); + } + + this.widget.setContext(overwrite, ...attachments); + } + + private _removeContext(attachments: IChatRequestVariableEntry[]) { + if (attachments.length) { + attachments.forEach(this._attachedContext.delete, this._attachedContext); + } + } + + private _clearAttachedContext() { + this._attachedContext.clear(); + } +} + +ChatWidget.CONTRIBS.push(ChatContextAttachments); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 43864b83521..a81f88f9367 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -10,9 +10,11 @@ import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { Command } from 'vs/editor/common/languages'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; @@ -39,7 +41,6 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC constructor( private readonly widget: IChatWidget, @ILabelService private readonly labelService: ILabelService, - @ILogService private readonly logService: ILogService, ) { super(); this._register(widget.inputEditor.onDidChangeModelContent(e => { @@ -48,12 +49,15 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC this._variables = coalesce(this._variables.map(ref => { const intersection = Range.intersectRanges(ref.range, c.range); if (intersection && !intersection.isEmpty()) { - // The reference text was changed, it's broken - const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); - this.widget.inputEditor.executeEdits(this.id, [{ - range: rangeToDelete, - text: '', - }]); + // The reference text was changed, it's broken. + // But if the whole reference range was deleted (eg history navigation) then don't try to change the editor. + if (!Range.containsRange(c.range, ref.range)) { + const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); + this.widget.inputEditor.executeEdits(this.id, [{ + range: rangeToDelete, + text: '', + }]); + } return null; } else if (Range.compareRangesUsingStarts(ref.range, c.range) > 0) { const delta = c.text.length - c.rangeLength; @@ -82,9 +86,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC setInputState(s: any): void { if (!Array.isArray(s)) { - // Something went wrong - this.logService.warn('ChatDynamicVariableModel.setInputState called with invalid state: ' + JSON.stringify(s)); - return; + s = []; } this._variables = s; @@ -97,18 +99,18 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC } private updateDecorations(): void { - this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map(r => ({ + this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map((r): IDecorationOptions => ({ range: r.range, hoverMessage: this.getHoverForReference(r) }))); } - private getHoverForReference(ref: IDynamicVariable): string | IMarkdownString { - const value = ref.data[0]; - if (URI.isUri(value.value)) { - return new MarkdownString(this.labelService.getUriLabel(value.value, { relative: true })); + private getHoverForReference(ref: IDynamicVariable): IMarkdownString | undefined { + const value = ref.data; + if (URI.isUri(value)) { + return new MarkdownString(this.labelService.getUriLabel(value, { relative: true })); } else { - return value.value.toString(); + return undefined; } } } @@ -125,6 +127,11 @@ function isSelectAndInsertFileActionContext(context: any): context is SelectAndI } export class SelectAndInsertFileAction extends Action2 { + static readonly Name = 'files'; + static readonly Item = { + label: localize('allFiles', 'All Files'), + description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), + }; static readonly ID = 'workbench.action.chat.selectAndInsertFile'; constructor() { @@ -151,20 +158,14 @@ export class SelectAndInsertFileAction extends Action2 { }; let options: IQuickAccessOptions | undefined; - const filesVariableName = 'files'; - const filesItem = { - label: localize('allFiles', 'All Files'), - description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), - }; // If we have a `files` variable, add an option to select all files in the picker. // This of course assumes that the `files` variable has the behavior that it searches // through files in the workspace. - if (chatVariablesService.hasVariable(filesVariableName)) { - options = { - providerOptions: { - additionPicks: [filesItem, { type: 'separator' }] - }, + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { + const providerOptions: AnythingQuickAccessProviderRunOptions = { + additionPicks: [SelectAndInsertFileAction.Item, { type: 'separator' }] }; + options = { providerOptions }; } // TODO: have dedicated UX for this instead of using the quick access picker const picks = await quickInputService.quickAccess.pick('', options); @@ -178,8 +179,8 @@ export class SelectAndInsertFileAction extends Action2 { const range = context.range; // Handle the special case of selecting all files - if (picks[0] === filesItem) { - const text = `#${filesVariableName}`; + if (picks[0] === SelectAndInsertFileAction.Item) { + const text = `#${SelectAndInsertFileAction.Name}`; const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); if (!success) { logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); @@ -206,17 +207,20 @@ export class SelectAndInsertFileAction extends Action2 { } context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ + id: 'vscode.file', range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, - data: [{ level: 'full', value: resource }] + data: resource }); } } registerAction2(SelectAndInsertFileAction); export interface IAddDynamicVariableContext { + id: string; widget: IChatWidget; range: IRange; - variableData: IChatRequestVariableValue[]; + variableData: IChatRequestVariableValue; + command?: Command; } function isAddDynamicVariableContext(context: any): context is IAddDynamicVariableContext { @@ -241,9 +245,40 @@ export class AddDynamicVariableAction extends Action2 { return; } + let range = context.range; + const variableData = context.variableData; + + const doCleanup = () => { + // Failed, remove the dangling variable prefix + context.widget.inputEditor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: context.range, text: `` }]); + }; + + // If this completion item has no command, return it directly + if (context.command) { + // Invoke the command on this completion item along with its args and return the result + const commandService = accessor.get(ICommandService); + const selection: string | undefined = await commandService.executeCommand(context.command.id, ...(context.command.arguments ?? [])); + if (!selection) { + doCleanup(); + return; + } + + // Compute new range and variableData + const insertText = ':' + selection; + const insertRange = new Range(range.startLineNumber, range.endColumn, range.endLineNumber, range.endColumn + insertText.length); + range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + insertText.length); + const editor = context.widget.inputEditor; + const success = editor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: insertRange, text: insertText + ' ' }]); + if (!success) { + doCleanup(); + return; + } + } + context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - range: context.range, - data: context.variableData + id: context.id, + range: range, + data: variableData }); } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts deleted file mode 100644 index 5df9a406f5b..00000000000 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts +++ /dev/null @@ -1,34 +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 'vs/base/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; - -class ChatHistoryVariables extends Disposable { - constructor( - @IChatVariablesService chatVariablesService: IChatVariablesService, - ) { - super(); - - this._register(chatVariablesService.registerVariable({ name: 'response', description: '', canTakeArgument: true, hidden: true }, async (message, arg, model, progress, token) => { - if (!arg) { - return undefined; - } - - const responseNum = parseInt(arg, 10); - const response = model.getRequests()[responseNum - 1].response; - if (!response) { - return undefined; - } - - return [{ level: 'full', value: response.response.asString() }]; - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ChatHistoryVariables, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts new file mode 100644 index 00000000000..8b0484bbe51 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper'; +import { CompletionContext, CompletionItem, CompletionItemKind } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { ChatAgentLocation, getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +class SlashCommandCompletions extends Disposable { + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'globalSlashCommands', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel) { + return null; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent) { + // No (classic) global slash commands when an agent is used + return; + } + + const slashCommands = this.chatSlashCommandService.getCommands(widget.location); + if (!slashCommands) { + return null; + } + + return { + suggestions: slashCommands.map((c, i): CompletionItem => { + const withSlash = `/${c.command}`; + return { + label: withSlash, + insertText: c.executeImmediately ? '' : `${withSlash} `, + detail: c.detail, + range: new Range(1, 1, 1, 1), + sortText: c.sortText ?? 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway, + command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + }; + }) + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); + +class AgentCompletions extends Disposable { + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgent', + triggerCharacters: ['@'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { + // Only one agent allowed + return; + } + + const range = computeCompletionRanges(model, position, /@\w*/g); + if (!range) { + return null; + } + + const agents = this.chatAgentService.getAgents() + .filter(a => !a.isDefault) + .filter(a => a.locations.includes(widget.location)); + + return { + suggestions: agents.map((agent, i): CompletionItem => { + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); + return { + // Leading space is important because detail has no space at the start by design + label: isDupe ? + { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : + agentLabel, + insertText: `${agentLabel} `, + detail: agent.description, + range: new Range(1, 1, 1, 1), + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: agent, widget } satisfies AssignSelectedAgentActionArgs] }, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + }; + }) + }; + } + })); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentSubcommand', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel) { + return; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + if (usedAgentIdx < 0) { + return; + } + + const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); + if (usedSubcommand) { + // Only one allowed + return; + } + + for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { + // Could allow text after 'position' + if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { + // No text allowed between agent and subcommand + return; + } + } + + const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; + return { + suggestions: usedAgent.agent.slashCommands.map((c, i): CompletionItem => { + const withSlash = `/${c.name}`; + return { + label: withSlash, + insertText: `${withSlash} `, + detail: c.description, + range, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + }; + }) + }; + } + })); + + // list subcommands when the query is empty, insert agent+subcommand + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentAndSubcommand', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + const viewModel = widget?.viewModel; + if (!widget || !viewModel) { + return; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const agents = this.chatAgentService.getAgents() + .filter(a => a.locations.includes(widget.location)); + + // When the input is only `/`, items are sorted by sortText. + // When typing, filterText is used to score and sort. + // The same list is refiltered/ranked while typing. + const getFilterText = (agent: IChatAgentData, command: string) => { + // This is hacking the filter algorithm to make @terminal /explain match worse than @workspace /explain by making its match index later in the string. + // When I type `/exp`, the workspace one should be sorted over the terminal one. + const dummyPrefix = agent.id === 'github.copilot.terminalPanel' ? `0000` : ``; + return `${chatSubcommandLeader}${dummyPrefix}${agent.name}.${command}`; + }; + + const justAgents: CompletionItem[] = agents + .filter(a => !a.isDefault) + .map(agent => { + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); + const detail = agent.description; + + return { + label: isDupe ? + { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : + agentLabel, + detail, + filterText: `${chatSubcommandLeader}${agent.name}`, + insertText: `${agentLabel} `, + range: new Range(1, 1, 1, 1), + kind: CompletionItemKind.Text, + sortText: `${chatSubcommandLeader}${agent.name}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, + }; + }); + + return { + suggestions: justAgents.concat( + agents.flatMap(agent => agent.slashCommands.map((c, i) => { + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); + const withSlash = `${chatSubcommandLeader}${c.name}`; + const item: CompletionItem = { + label: { label: withSlash, description: agentLabel, detail: isDupe ? ` (${agent.publisherDisplayName})` : undefined }, + filterText: getFilterText(agent, c.name), + commitCharacters: [' '], + insertText: `${agentLabel} ${withSlash} `, + detail: `(${agentLabel}) ${c.description ?? ''}`, + range: new Range(1, 1, 1, 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway + sortText: `${chatSubcommandLeader}${agent.name}${c.name}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, + }; + + if (agent.isDefault) { + // default agent isn't mentioned nor inserted + item.label = withSlash; + item.insertText = `${withSlash} `; + item.detail = c.description; + } + + return item; + }))) + }; + } + })); + } + + private getAgentCompletionDetails(agent: IChatAgentData): { label: string; isDupe: boolean } { + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); + const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; + const isDupe = isAllowed && this.chatAgentService.agentHasDupeName(agent.id); + return { label: agentLabel, isDupe }; + } +} +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); + +interface AssignSelectedAgentActionArgs { + agent: IChatAgentData; + widget: IChatWidget; +} + +class AssignSelectedAgentAction extends Action2 { + static readonly ID = 'workbench.action.chat.assignSelectedAgent'; + + constructor() { + super({ + id: AssignSelectedAgentAction.ID, + title: '' // not displayed + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const arg: AssignSelectedAgentActionArgs = args[0]; + if (!arg || !arg.widget || !arg.agent) { + return; + } + + arg.widget.lastSelectedAgent = arg.agent; + } +} +registerAction2(AssignSelectedAgentAction); + +class BuiltinDynamicCompletions extends Disposable { + private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatDynamicCompletions', + triggerCharacters: [chatVariableLeader], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.supportsFileReferences) { + return null; + } + + const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); + if (!range) { + return null; + } + + const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); + return { + suggestions: [ + { + label: `${chatVariableLeader}file`, + insertText: `${chatVariableLeader}file:`, + detail: localize('pickFileLabel', "Pick a file"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, + sortText: 'z' + } satisfies CompletionItem + ] + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); + +function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp, onlyOnWordStart = false): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { + const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); + if (!varWord && model.getWordUntilPosition(position).word) { + // inside a "normal" word + return; + } + if (varWord && onlyOnWordStart) { + const wordBefore = model.getWordUntilPosition({ lineNumber: position.lineNumber, column: varWord.startColumn }); + if (wordBefore.word) { + // inside a word + return; + } + } + + let insert: Range; + let replace: Range; + if (!varWord) { + insert = replace = Range.fromPositions(position); + } else { + insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); + replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); + } + + return { insert, replace, varWord }; +} + +class VariableCompletions extends Disposable { + + private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + @IConfigurationService configService: IConfigurationService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatVariables', + triggerCharacters: [chatVariableLeader], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + + const locations = new Set(); + locations.add(ChatAgentLocation.Panel); + + for (const value of Object.values(ChatAgentLocation)) { + if (typeof value === 'string' && configService.getValue(`chat.experimental.variables.${value}`)) { + locations.add(value); + } + } + + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !locations.has(widget.location)) { + return null; + } + + const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef, true); + if (!range) { + return null; + } + + const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); + const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; + + const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); + const variableItems = Array.from(this.chatVariablesService.getVariables(widget.location)) + // This doesn't look at dynamic variables like `file`, where multiple makes sense. + .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) + .filter(v => !v.isSlow || slowSupported) + .map((v): CompletionItem => { + const withLeader = `${chatVariableLeader}${v.name}`; + return { + label: withLeader, + range, + insertText: withLeader + ' ', + detail: v.description, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + sortText: 'z' + }; + }); + + return { + suggestions: variableItems + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 45e19d62a5e..b72abe680df 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -3,36 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } from 'vs/editor/common/languages'; -import { ITextModel } from 'vs/editor/common/model'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { localize } from 'vs/nls'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; -import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { SelectAndInsertFileAction, dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; const decorationDescription = 'chat'; const placeholderDecorationType = 'chat-session-detail'; @@ -66,6 +51,7 @@ class InputEditorDecorations extends Disposable { this.updateInputEditorDecorations(); this._register(this.widget.inputEditor.onDidChangeModelContent(() => this.updateInputEditorDecorations())); + this._register(this.widget.onDidChangeParsedInput(() => this.updateInputEditorDecorations())); this._register(this.widget.onDidChangeViewModel(() => { this.registerViewModelListeners(); this.previouslyUsedAgents.clear(); @@ -137,7 +123,7 @@ class InputEditorDecorations extends Disposable { }, renderOptions: { after: { - contentText: viewModel.inputPlaceholder ?? defaultAgent?.description ?? '', + contentText: viewModel.inputPlaceholder || (defaultAgent?.description ?? ''), color: this.getPlaceholderColor() } } @@ -189,8 +175,8 @@ class InputEditorDecorations extends Disposable { } } - const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); - if (onlyAgentCommandAndWhitespace) { + const onlyAgentAndAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); + if (onlyAgentAndAgentCommandAndWhitespace) { // Agent reference and subcommand with no other text - show the placeholder const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name)); const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder; @@ -207,15 +193,30 @@ class InputEditorDecorations extends Disposable { } } + const onlyAgentCommandAndWhitespace = agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentSubcommandPart); + if (onlyAgentCommandAndWhitespace) { + // Agent subcommand with no other text - show the placeholder + if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) { + placeholderDecoration = [{ + range: getRangeForPlaceholder(agentSubcommandPart), + renderOptions: { + after: { + contentText: agentSubcommandPart.command.description, + color: this.getPlaceholderColor(), + } + } + }]; + } + } + this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, placeholderDecoration ?? []); const textDecorations: IDecorationOptions[] | undefined = []; if (agentPart) { - const agentHover = `(${agentPart.agent.name}) ${agentPart.agent.description}`; - textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) }); - if (agentSubcommandPart) { - textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); - } + textDecorations.push({ range: agentPart.editorRange }); + } + if (agentSubcommandPart) { + textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); } if (slashCommandPart) { @@ -241,12 +242,22 @@ class InputEditorSlashCommandMode extends Disposable { private readonly widget: IChatWidget ) { super(); + this._register(this.widget.onDidChangeAgent(e => { + if (e.slashCommand && e.slashCommand.isSticky || !e.slashCommand && e.agent.metadata.isSticky) { + this.repopulateAgentCommand(e.agent, e.slashCommand); + } + })); this._register(this.widget.onDidSubmitAgent(e => { this.repopulateAgentCommand(e.agent, e.slashCommand); })); } private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) { + // Make sure we don't repopulate if the user already has something in the input + if (this.widget.inputEditor.getValue().trim()) { + return; + } + let value: string | undefined; if (slashCommand && slashCommand.isSticky) { value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `; @@ -263,367 +274,6 @@ class InputEditorSlashCommandMode extends Disposable { ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandMode); -class SlashCommandCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'globalSlashCommands', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent) { - // No (classic) global slash commands when an agent is used - return; - } - - const slashCommands = this.chatSlashCommandService.getCommands(); - if (!slashCommands) { - return null; - } - - return { - suggestions: slashCommands.map((c, i) => { - const withSlash = `/${c.command}`; - return { - label: withSlash, - insertText: c.executeImmediately ? '' : `${withSlash} `, - detail: c.detail, - range: new Range(1, 1, 1, 1), - sortText: c.sortText ?? 'a'.repeat(i + 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, - }; - }) - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); - -class AgentCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgent', - triggerCharacters: ['@'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { - // Only one agent allowed - return; - } - - const range = computeCompletionRanges(model, position, /@\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => !a.isDefault) - .filter(a => a.locations.includes(widget.location)); - - return { - suggestions: agents.map((a, i) => { - const withAt = `@${a.name}`; - const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id); - return { - // Leading space is important because detail has no space at the start by design - label: isDupe ? - { label: withAt, description: a.description, detail: ` (${a.id})` } : - withAt, - insertText: `${withAt} `, - detail: a.description, - range: new Range(1, 1, 1, 1), - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: a, widget } satisfies AssignSelectedAgentActionArgs] }, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - }; - }) - }; - } - })); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentSubcommand', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); - if (usedAgentIdx < 0) { - return; - } - - const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); - if (usedSubcommand) { - // Only one allowed - return; - } - - for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { - // Could allow text after 'position' - if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { - // No text allowed between agent and subcommand - return; - } - } - - const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - return { - suggestions: usedAgent.agent.slashCommands.map((c, i) => { - const withSlash = `/${c.name}`; - return { - label: withSlash, - insertText: `${withSlash} `, - detail: c.description, - range, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - }; - }) - }; - } - })); - - // list subcommands when the query is empty, insert agent+subcommand - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentAndSubcommand', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - const viewModel = widget?.viewModel; - if (!widget || !viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => a.locations.includes(widget.location)); - - const justAgents: CompletionItem[] = agents - .filter(a => !a.isDefault) - .map(agent => { - const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id); - const detail = agent.description; - const agentLabel = `${chatAgentLeader}${agent.name}`; - - return { - label: isDupe ? - { label: agentLabel, description: agent.description, detail: ` (${agent.id})` } : - agentLabel, - detail, - filterText: `${chatSubcommandLeader}${agent.name}`, - insertText: `${agentLabel} `, - range: new Range(1, 1, 1, 1), - kind: CompletionItemKind.Text, - sortText: `${chatSubcommandLeader}${agent.name}`, - }; - }); - - return { - suggestions: justAgents.concat( - agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const agentLabel = `${chatAgentLeader}${agent.name}`; - const withSlash = `${chatSubcommandLeader}${c.name}`; - return { - label: { label: withSlash, description: agentLabel }, - filterText: `${chatSubcommandLeader}${agent.name}${c.name}`, - commitCharacters: [' '], - insertText: `${agentLabel} ${withSlash} `, - detail: `(${agentLabel}) ${c.description ?? ''}`, - range: new Range(1, 1, 1, 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: `${chatSubcommandLeader}${agent.name}${c.name}`, - } satisfies CompletionItem; - }))) - }; - } - })); - } -} -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); - -interface AssignSelectedAgentActionArgs { - agent: IChatAgentData; - widget: IChatWidget; -} - -class AssignSelectedAgentAction extends Action2 { - static readonly ID = 'workbench.action.chat.assignSelectedAgent'; - - constructor() { - super({ - id: AssignSelectedAgentAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const arg: AssignSelectedAgentActionArgs = args[0]; - if (!arg || !arg.widget || !arg.agent) { - return; - } - - arg.widget.lastSelectedAgent = arg.agent; - } -} -registerAction2(AssignSelectedAgentAction); - -class BuiltinDynamicCompletions extends Disposable { - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicCompletions', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef); - if (!range) { - return null; - } - - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - return { - suggestions: [ - { - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - detail: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - } - ] - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); - -function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { - const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - // inside a "normal" word - return; - } - - let insert: Range; - let replace: Range; - if (!varWord) { - insert = replace = Range.fromPositions(position); - } else { - insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); - replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); - } - - return { insert, replace, varWord }; -} - -class VariableCompletions extends Disposable { - - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatVariables', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); - if (!range) { - return null; - } - - const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); - const variableItems = Array.from(this.chatVariablesService.getVariables()) - // This doesn't look at dynamic variables like `file`, where multiple makes sense. - .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) - .map(v => { - const withLeader = `${chatVariableLeader}${v.name}`; - return { - label: withLeader, - range, - insertText: withLeader + ' ', - detail: v.description, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: 'z' - }; - }); - - return { - suggestions: variableItems - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); - class ChatTokenDeleter extends Disposable { public readonly id = 'chatTokenDeleter'; @@ -651,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, ChatAgentLocation.Panel, { selectedAgent: previousSelectedAgent }); + const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent }); // 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 ChatRequestVariablePart); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts new file mode 100644 index 00000000000..893353b52f1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.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 { DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatEditorHoverWrapper } from 'vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper'; +import { IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import * as nls from 'vs/nls'; + +export class ChatAgentHoverParticipant implements IEditorHoverParticipant { + + public readonly hoverOrdinal: number = 1; + + constructor( + private readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, + ) { } + + public computeSync(anchor: HoverAnchor, _lineDecorations: IModelDecoration[]): ChatAgentHoverPart[] { + if (!this.editor.hasModel()) { + return []; + } + + const widget = this.chatWidgetService.getWidgetByInputUri(this.editor.getModel().uri); + if (!widget) { + return []; + } + + const { agentPart } = extractAgentAndCommand(widget.parsedInput); + if (!agentPart) { + return []; + } + + if (Range.containsPosition(agentPart.editorRange, anchor.range.getStartPosition())) { + return [new ChatAgentHoverPart(this, Range.lift(agentPart.editorRange), agentPart.agent)]; + } + + return []; + } + + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ChatAgentHoverPart[]): IRenderedHoverParts { + if (!hoverParts.length) { + return new RenderedHoverParts([]); + } + + const disposables = new DisposableStore(); + const hover = disposables.add(this.instantiationService.createInstance(ChatAgentHover)); + disposables.add(hover.onDidChangeContents(() => context.onContentsChanged())); + const hoverPart = hoverParts[0]; + const agent = hoverPart.agent; + hover.setAgent(agent.id); + + const actions = getChatAgentHoverOptions(() => agent, this.commandService).actions; + const wrapper = this.instantiationService.createInstance(ChatEditorHoverWrapper, hover.domNode, actions); + const wrapperNode = wrapper.domNode; + context.fragment.appendChild(wrapperNode); + const renderedHoverPart: IRenderedHoverPart = { + hoverPart, + hoverElement: wrapperNode, + dispose() { disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } + + public getAccessibleContent(hoverPart: ChatAgentHoverPart): string { + return nls.localize('hoverAccessibilityChatAgent', 'There is a chat agent hover part here.'); + + } +} + +export class ChatAgentHoverPart implements IHoverPart { + + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range, + public readonly agent: IChatAgentData + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +HoverParticipantRegistry.register(ChatAgentHoverParticipant); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts new file mode 100644 index 00000000000..5cbc7d932cc --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/editorHoverWrapper'; +import * as dom from 'vs/base/browser/dom'; +import { IHoverAction } from 'vs/base/browser/ui/hover/hover'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +const $ = dom.$; +const h = dom.h; + +/** + * This borrows some of HoverWidget so that a chat editor hover can be rendered in the same way as a workbench hover. + * Maybe it can be reusable in a generic way. + */ +export class ChatEditorHoverWrapper { + public readonly domNode: HTMLElement; + + constructor( + hoverContentElement: HTMLElement, + actions: IHoverAction[] | undefined, + @IKeybindingService private readonly keybindingService: IKeybindingService, + ) { + const hoverElement = h( + '.chat-editor-hover-wrapper@root', + [h('.chat-editor-hover-wrapper-content@content')]); + this.domNode = hoverElement.root; + hoverElement.content.appendChild(hoverContentElement); + + if (actions && actions.length > 0) { + const statusBarElement = $('.hover-row.status-bar'); + const actionsElement = $('.actions'); + actions.forEach(action => { + const keybinding = this.keybindingService.lookupKeybinding(action.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + HoverAction.render(actionsElement, { + label: action.label, + commandId: action.commandId, + run: e => { + action.run(e); + }, + iconClass: action.iconClass + }, keybindingLabel); + }); + statusBarElement.appendChild(actionsElement); + this.domNode.appendChild(statusBarElement); + } + } +} diff --git a/extensions/microsoft-authentication/src/browser/crypto.ts b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css similarity index 85% rename from extensions/microsoft-authentication/src/browser/crypto.ts rename to src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css index 37a1b43361f..d95fd395255 100644 --- a/extensions/microsoft-authentication/src/browser/crypto.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css @@ -3,4 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export const crypto = globalThis.crypto; +.chat-editor-hover-wrapper-content { + padding: 2px 8px; +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 4360f8f1cc2..6e67ee583a9 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -39,18 +39,30 @@ .interactive-item-container .header .user { display: flex; align-items: center; - gap: 6px; + gap: 8px; + + /* + Rendering the avatar icon as round makes it a little larger than the .user container. + Add padding so that the focus outline doesn't run into it, and counteract it with a negative margin so it doesn't actually take up any extra space */ + padding: 2px; + margin: -2px; } .interactive-item-container .header .username { margin: 0; - font-size: 12px; + font-size: 13px; font-weight: 600; } -.interactive-item-container .header .detail-container { - font-size: 0.9em; - opacity: 0.7; +.interactive-item-container .detail-container { + font-size: 12px; + color: var(--vscode-descriptionForeground); + overflow: hidden; +} + +.interactive-item-container .detail-container .detail .agentOrSlashCommandDetected A { + cursor: pointer; + color: var(--vscode-textLink-foreground); } .interactive-item-container .chat-animated-ellipsis { @@ -101,7 +113,7 @@ width: 24px; height: 24px; border-radius: 50%; - outline: 1px solid var(--vscode-chat-requestBorder) + outline: 1px solid var(--vscode-chat-requestBorder); } .interactive-item-container .header .avatar.codicon-avatar { @@ -124,23 +136,6 @@ font-size: 14px; } -.interactive-item-container .header .agent-avatar-container { - margin-left: -30px; - transition: margin 0.15s ease-out; - transition-delay: 0.5s; - z-index: -1; -} - -.interactive-item-container .header .agent-avatar-container.loading { - margin-left: 0px; - z-index: 1; -} - -.interactive-item-container .header .agent-avatar-container.complete { - margin-left: -12px; - z-index: 1; -} - .monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar, .monaco-list:not(:focus-within) .monaco-list-row .interactive-item-container:not(:hover) .header .monaco-toolbar, .monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar .action-label, @@ -182,6 +177,10 @@ width: 100%; } +.interactive-item-container > .value .chat-used-context { + margin-bottom: 8px; +} + .interactive-item-container .value .rendered-markdown table { width: 100%; text-align: left; @@ -219,7 +218,6 @@ .interactive-request { border-bottom: 1px solid var(--vscode-chat-requestBorder); border-top: 1px solid var(--vscode-chat-requestBorder); - background-color: var(--vscode-chat-requestBackground); } .hc-black .interactive-request, @@ -230,7 +228,7 @@ .interactive-item-container .value { white-space: normal; - word-wrap: break-word; + overflow-wrap: anywhere; } .interactive-item-container .value > :last-child.rendered-markdown > :last-child { @@ -257,10 +255,17 @@ } .interactive-item-container .value .rendered-markdown p { - margin: 0 0 16px 0; line-height: 1.5em; } +.interactive-item-container .value > .rendered-markdown p { + margin: 0 0 16px 0; +} + +.interactive-item-container .value > .rendered-markdown li > p { + margin: 0; +} + .interactive-item-container .value .rendered-markdown ul { padding-inline-start: 24px; } @@ -273,6 +278,10 @@ line-height: 1.3rem; } +.interactive-item-container .value .rendered-markdown img { + max-width: 100%; +} + .interactive-item-container .monaco-tokenized-source, .interactive-item-container code { font-family: var(--monaco-monospace-font); @@ -318,10 +327,14 @@ min-height: 0; } -.interactive-item-container.interactive-item-compact .value .rendered-markdown p { +.interactive-item-container.interactive-item-compact .value > .rendered-markdown p { margin: 0 0 8px 0; } +.interactive-item-container.interactive-item-compact .value > .rendered-markdown li > p { + margin: 0; +} + .interactive-item-container.interactive-item-compact .value .rendered-markdown h1 { margin: 8px 0; @@ -335,8 +348,32 @@ margin: 8px 0; } -.interactive-item-container.interactive-item-compact .value .rendered-markdown p { - margin: 0 0 8px 0; +.interactive-item-container.minimal { + flex-direction: row; +} + +.interactive-item-container.minimal .column.left { + padding-top: 2px; + display: inline-block; + flex-grow: 0; +} + +.interactive-item-container.minimal .column.right { + display: inline-block; + flex-grow: 1; +} + +.interactive-item-container.minimal .user > .username { + display: none; +} + +.interactive-item-container.minimal .detail-container { + font-size: unset; +} + +.interactive-item-container.minimal > .header { + position: absolute; + right: 0; } .interactive-session .interactive-input-and-execute-toolbar { @@ -355,6 +392,7 @@ .interactive-session .interactive-input-part.compact .interactive-input-and-execute-toolbar { margin-bottom: 0; + border-radius: 2px; } .interactive-session .interactive-input-and-side-toolbar { @@ -367,7 +405,7 @@ border-color: var(--vscode-focusBorder); } -.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .mtk1 { +.interactive-input-and-execute-toolbar .monaco-editor .mtk1 { color: var(--vscode-input-foreground); } @@ -410,6 +448,18 @@ padding-inline-start: unset; } +.chat-notification-widget .chat-info-codicon, +.chat-notification-widget .chat-error-codicon, +.chat-notification-widget .chat-warning-codicon { + display: flex; + align-items: start; + gap: 8px; +} + +.interactive-item-container .value .chat-notification-widget .rendered-markdown p { + margin: 0; +} + .interactive-response .interactive-response-error-details { display: flex; align-items: start; @@ -420,14 +470,45 @@ margin-bottom: 0px; } +.chat-notification-widget .chat-info-codicon .codicon, +.chat-notification-widget .chat-error-codicon .codicon, +.chat-notification-widget .chat-warning-codicon .codicon { + margin-top: 2px; +} + .interactive-response .interactive-response-error-details .codicon { margin-top: 1px; } +.chat-used-context-list .codicon-warning { + color: var(--vscode-notificationsWarningIcon-foreground); /* Have to override default styles which apply to all lists */ +} + +.chat-used-context-list .monaco-icon-label-container { + color: var(--vscode-interactive-session-foreground); +} + +.chat-attached-context .chat-attached-context-attachment .monaco-icon-name-container.warning, +.chat-attached-context .chat-attached-context-attachment .monaco-icon-suffix-container.warning, +.chat-used-context-list .monaco-icon-name-container.warning, +.chat-used-context-list .monaco-icon-suffix-container.warning { + color: var(--vscode-notificationsWarningIcon-foreground); +} + +.chat-attached-context .chat-attached-context-attachment.show-file-icons.warning { + border-color: var(--vscode-notificationsWarningIcon-foreground); +} + +.chat-notification-widget .chat-warning-codicon .codicon-warning { + color: var(--vscode-notificationsWarningIcon-foreground) !important; /* Have to override default styles which apply to all lists */ +} + +.chat-notification-widget .chat-error-codicon .codicon-error, .interactive-response .interactive-response-error-details .codicon-error { color: var(--vscode-errorForeground) !important; /* Have to override default styles which apply to all lists */ } +.chat-notification-widget .chat-info-codicon .codicon-info, .interactive-response .interactive-response-error-details .codicon-info { color: var(--vscode-notificationsInfoIcon-foreground) !important; /* Have to override default styles which apply to all lists */ } @@ -441,14 +522,63 @@ .interactive-session .interactive-input-part.compact { margin: 0; - padding: 6px 0px; + padding: 8px 0 0 0 } -.interactive-session .chat-implicit-context { - padding: 8px 8px 13px; - margin-bottom: -5px; - border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); - border-radius: 6px 6px 0px 0px; +.interactive-session .chat-attached-context .chat-attached-context-attachment { + display: flex; + gap: 4px; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover { + cursor: pointer; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button { + display: flex; + align-items: center; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container { + display: flex; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { + display: flex !important; + align-items: center !important; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .monaco-button.codicon.codicon-close, +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-close { + color: var(--vscode-descriptionForeground); + cursor: pointer; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .codicon { + padding-left: 4px; +} + +.interactive-session .chat-attached-context { + padding: 0 0 8px 0; + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.interactive-session .interactive-item-container.interactive-request .chat-attached-context { + margin-top: -8px; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment { + padding: 2px; + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + border-radius: 4px; + height: 18px; + max-width: 100%; +} + +.interactive-session .interactive-item-container.interactive-request .chat-attached-context .chat-attached-context-attachment { + padding-right: 6px; } .interactive-session-followups { @@ -480,6 +610,12 @@ display: block; color: var(--vscode-textLink-foreground); font-size: 12px; + + /* clamp to max 3 lines */ + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } .interactive-session .interactive-input-part .interactive-input-followups .interactive-session-followups code { @@ -580,11 +716,19 @@ .interactive-item-container .chat-resource-widget { background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); +} + +.interactive-item-container .chat-resource-widget, +.interactive-item-container .chat-agent-widget .monaco-button { border-radius: 4px; - white-space: nowrap; padding: 1px 3px; } +.interactive-item-container .chat-agent-widget .monaco-text-button { + display: inline; + border: none; +} + .interactive-session .chat-used-context.chat-used-context-collapsed .chat-used-context-list { display: none; } @@ -592,15 +736,20 @@ .interactive-session .chat-used-context { display: flex; flex-direction: column; - gap: 6px; + gap: 2px; } .interactive-response-progress-tree, +.interactive-item-container .chat-notification-widget, .interactive-session .chat-used-context-list { border: 1px solid var(--vscode-chat-requestBorder); border-radius: 4px; margin-bottom: 8px; - padding: 4px; + padding: 6px 8px; +} + +.interactive-item-container .chat-notification-widget { + padding: 8px 12px; } .interactive-session .chat-used-context-list .monaco-list .monaco-list-row { @@ -609,8 +758,7 @@ .interactive-session .chat-used-context-label { font-size: 12px; - color: var(--vscode-foreground); - opacity: 0.8; + color: var(--vscode-descriptionForeground); user-select: none; } @@ -621,13 +769,21 @@ .interactive-session .chat-used-context-label .monaco-button { /* unset Button styles */ display: inline-flex; + gap: 4px; width: 100%; border: none; - padding: 0; + border-radius: 4px; + padding: 4px 8px 4px 0; text-align: initial; justify-content: initial; } +.interactive-session .chat-used-context-label .monaco-button:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-foreground); + +} + .interactive-session .chat-used-context-label .monaco-text-button:focus { outline: none; } @@ -647,11 +803,12 @@ } .interactive-item-container .rendered-markdown.progress-step > p { + color: var(--vscode-descriptionForeground); + font-size: 12px; display: flex; - gap: 5px; + gap: 8px; align-items: center; - opacity: 0.7; - margin-bottom: 4px; + margin-bottom: 6px; } .interactive-item-container .rendered-markdown.progress-step > p .codicon { @@ -660,7 +817,6 @@ } .interactive-item-container .rendered-markdown.progress-step > p .codicon.codicon-check { - font-size: 14px; color: var(--vscode-debugIcon-startForeground) !important; } @@ -669,7 +825,14 @@ margin-bottom: 16px; } -.interactive-item-container .chat-command-button .monaco-button { +.interactive-item-container .chat-notification-widget { + display: flex; + flex-direction: row; + gap: 6px; +} + +.interactive-item-container .chat-command-button .monaco-button, +.chat-confirmation-widget .chat-confirmation-buttons-container .monaco-button { text-align: left; width: initial; padding: 4px 8px; @@ -679,3 +842,19 @@ margin-left: 0; margin-top: 1px; } + +.chat-code-citation-label { + opacity: 0.7; + white-space: pre-wrap; +} + +.chat-code-citation-button-container { + display: inline; +} + +.chat-code-citation-button-container .monaco-button { + display: inline; + border: none; + padding: 0; + color: var(--vscode-textLink-foreground); +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css new file mode 100644 index 00000000000..29e38f48cad --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-agent-hover { + line-height: unset; + padding: 6px 0px; +} + +.chat-agent-hover-header { + display: flex; + gap: 8px; + margin-bottom: 4px; +} + +.chat-agent-hover-icon img, +.chat-agent-hover-icon .codicon { + width: 32px; + height: 32px; + border-radius: 50%; + outline: 1px solid var(--vscode-chat-requestBorder); +} + +.chat-agent-hover .chat-agent-hover-icon .codicon { + font-size: 23px !important; /* Override workbench hover styles */ + display: flex; + justify-content: center; + align-items: center; +} + +.chat-agent-hover-publisher { + display: flex; + gap: 4px; +} + +.chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { + color: var(--vscode-extensionIcon-verifiedForeground); +} + +.chat-agent-hover .extension-verified-publisher { + display: none; +} + +.chat-agent-hover.verifiedPublisher .extension-verified-publisher { + display: flex; +} + +.chat-agent-hover .chat-agent-hover-warning .codicon { + color: var(--vscode-notificationsWarningIcon-foreground) !important; + margin-right: 3px; +} + +.chat-agent-hover.allowedName .chat-agent-hover-warning { + display: none; +} + +.chat-agent-hover-header .chat-agent-hover-name { + font-size: 14px; + font-weight: 600; +} + +.chat-agent-hover-header .chat-agent-hover-details { + font-size: 12px; +} + +.chat-agent-hover-extension { + display: flex; + gap: 6px; + color: var(--vscode-descriptionForeground); +} + +.chat-agent-hover.noExtensionName .chat-agent-hover-separator, +.chat-agent-hover.noExtensionName .chat-agent-hover-extension-name { + display: none; +} + +.chat-agent-hover-separator { + opacity: 0.7; +} + +.chat-agent-hover-description, +.chat-agent-hover-warning { + font-size: 13px; +} diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index f55eb1ffd9a..449ff1bc2dd 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -6,13 +6,13 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatProgressRenderableResponseContent, IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentMarkdownContentWithVulnerability, IChatAgentVulnerabilityDetails, IChatContentInlineReference, IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI -export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { - const result: Exclude[] = []; +export function annotateSpecialMarkdownContent(response: ReadonlyArray): IChatProgressRenderableResponseContent[] { + const result: IChatProgressRenderableResponseContent[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { @@ -20,17 +20,21 @@ export function annotateSpecialMarkdownContent(response: ReadonlyArray${item.content.value}`; if (previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + // Since this is inside a codeblock, it needs to be merged into the previous markdown content. + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[result.length - 1] = { content: merged, kind: 'markdownContent' }; } else { result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); } @@ -101,4 +105,3 @@ export function extractVulnerabilitiesFromText(text: string): { newText: string; return { newText, vulnerabilities }; } - diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 2aa2893de49..eb1bb9b93fc 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -3,26 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { findLast } from 'vs/base/common/arraysFind'; +import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import { IObservable } from 'vs/base/common/observable'; +import { observableValue } from 'vs/base/common/observableInternal/base'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { ProviderResult } from 'vs/editor/common/languages'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Command, ProviderResult } from 'vs/editor/common/languages'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { asJson, IRequestService } from 'vs/platform/request/common/request'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; +import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc export interface IChatAgentHistoryEntry { request: IChatAgentRequest; - response: ReadonlyArray; + response: ReadonlyArray; result: IChatAgentResult; } @@ -39,6 +50,7 @@ export namespace ChatAgentLocation { case 'panel': return ChatAgentLocation.Panel; case 'terminal': return ChatAgentLocation.Terminal; case 'notebook': return ChatAgentLocation.Notebook; + case 'editor': return ChatAgentLocation.Editor; } return ChatAgentLocation.Panel; } @@ -47,21 +59,43 @@ export namespace ChatAgentLocation { export interface IChatAgentData { id: string; name: string; + fullName?: string; description?: string; + when?: string; extensionId: ExtensionIdentifier; + extensionPublisherId: string; + /** This is the extension publisher id, or, in the case of a dynamically registered participant (remote agent), whatever publisher name we have for it */ + publisherDisplayName?: string; + extensionDisplayName: string; /** The agent invoked when no agent is specified */ isDefault?: boolean; + /** This agent is not contributed in package.json, but is registered dynamically */ + isDynamic?: boolean; metadata: IChatAgentMetadata; slashCommands: IChatAgentCommand[]; - defaultImplicitVariables?: string[]; locations: ChatAgentLocation[]; } export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; - provideSampleQuestions?(token: CancellationToken): ProviderResult; + provideWelcomeMessage?(location: ChatAgentLocation, token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; + provideSampleQuestions?(location: ChatAgentLocation, token: CancellationToken): ProviderResult; +} + +export interface IChatParticipantDetectionResult { + participant: string; + command?: string; +} + +export interface IChatParticipantMetadata { + participant: string; + command?: string; + description?: string; +} + +export interface IChatParticipantDetectionProvider { + provideParticipantDetection(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation; participants: IChatParticipantMetadata[] }, token: CancellationToken): Promise; } export type IChatAgent = IChatAgentData & IChatAgentImplementation; @@ -84,7 +118,6 @@ export interface IChatAgentMetadata { helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; isSecondary?: boolean; // Invoked by ctrl/cmd+enter - fullName?: string; icon?: URI; iconDark?: URI; themeIcon?: ThemeIcon; @@ -93,6 +126,7 @@ export interface IChatAgentMetadata { followupPlaceholder?: string; isSticky?: boolean; requester?: IChatRequesterInformation; + supportsSlowVariables?: boolean; } @@ -102,8 +136,19 @@ export interface IChatAgentRequest { agentId: string; command?: string; message: string; + attempt?: number; + enableCommandDetection?: boolean; variables: IChatRequestVariableData; location: ChatAgentLocation; + locationData?: IChatLocationData; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; +} + +export interface IChatQuestion { + readonly prompt: string; + readonly participant?: string; + readonly command?: string; } export interface IChatAgentResult { @@ -114,6 +159,7 @@ export interface IChatAgentResult { }; /** Extra properties that the agent can use to identify a result */ readonly metadata?: { readonly [key: string]: any }; + nextQuestion?: IChatQuestion; } export const IChatAgentService = createDecorator('chatAgentService'); @@ -123,6 +169,15 @@ interface IChatAgentEntry { impl?: IChatAgentImplementation; } +export interface IChatAgentCompletionItem { + id: string; + name?: string; + fullName?: string; + icon?: ThemeIcon; + value: unknown; + command?: Command; +} + export interface IChatAgentService { _serviceBrand: undefined; /** @@ -132,13 +187,28 @@ export interface IChatAgentService { registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable; + getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise; + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable; + detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined>; invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getAgent(id: string): IChatAgentData | undefined; + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined; getAgents(): IChatAgentData[]; getActivatedAgents(): Array; getAgentsByName(name: string): IChatAgentData[]; + agentHasDupeName(id: string): boolean; + + /** + * Get the default agent (only if activated) + */ getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined; + + /** + * Get the default agent data that has been contributed (may not be activated yet) + */ + getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined; getSecondaryAgent(): IChatAgentData | undefined; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } @@ -149,14 +219,18 @@ export class ChatAgentService implements IChatAgentService { declare _serviceBrand: undefined; - private _agents: IChatAgentEntry[] = []; + private _agents = new Map(); private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + private readonly _hasDefaultAgent: IContextKey; + constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { } + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService); + } registerAgent(id: string, data: IChatAgentData): IDisposable { const existingAgent = this.getAgent(id); @@ -173,15 +247,15 @@ export class ChatAgentService implements IChatAgentService { } }; const entry = { data }; - this._agents.push(entry); + this._agents.set(id, entry); return toDisposable(() => { - this._agents = this._agents.filter(a => a !== entry); + this._agents.delete(id); this._onDidChangeAgents.fire(undefined); }); } registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable { - const entry = this._getAgentEntry(id); + const entry = this._agents.get(id); if (!entry) { throw new Error(`Unknown agent: ${JSON.stringify(id)}`); } @@ -190,28 +264,50 @@ export class ChatAgentService implements IChatAgentService { throw new Error(`Agent already has implementation: ${JSON.stringify(id)}`); } + if (entry.data.isDefault) { + this._hasDefaultAgent.set(true); + } + entry.impl = agentImpl; this._onDidChangeAgents.fire(new MergedChatAgent(entry.data, agentImpl)); return toDisposable(() => { entry.impl = undefined; this._onDidChangeAgents.fire(undefined); + + if (entry.data.isDefault) { + this._hasDefaultAgent.set(false); + } }); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { + data.isDynamic = true; const agent = { data, impl: agentImpl }; - this._agents.push(agent); + this._agents.set(data.id, agent); this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); return toDisposable(() => { - this._agents = this._agents.filter(a => a !== agent); + this._agents.delete(data.id); this._onDidChangeAgents.fire(undefined); }); } + private _agentCompletionProviders = new Map Promise>(); + + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise) { + this._agentCompletionProviders.set(id, provider); + return { + dispose: () => { this._agentCompletionProviders.delete(id); } + }; + } + + async getAgentCompletionItems(id: string, query: string, token: CancellationToken) { + return await this._agentCompletionProviders.get(id)?.(query, token) ?? []; + } + updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { - const agent = this._getAgentEntry(id); + const agent = this._agents.get(id); if (!agent?.impl) { throw new Error(`No activated agent with id ${JSON.stringify(id)} registered`); } @@ -220,7 +316,11 @@ export class ChatAgentService implements IChatAgentService { } getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined { - return this.getActivatedAgents().find(a => !!a.isDefault && a.locations.includes(location)); + return findLast(this.getActivatedAgents(), a => !!a.isDefault && a.locations.includes(location)); + } + + getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { + return this.getAgents().find(a => !!a.isDefault && a.locations.includes(location)); } getSecondaryAgent(): IChatAgentData | undefined { @@ -228,24 +328,41 @@ export class ChatAgentService implements IChatAgentService { return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - private _getAgentEntry(id: string): IChatAgentEntry | undefined { - return this._agents.find(a => a.data.id === id); + getAgent(id: string): IChatAgentData | undefined { + if (!this._agentIsEnabled(id)) { + return; + } + + return this._agents.get(id)?.data; } - getAgent(id: string): IChatAgentData | undefined { - return this._getAgentEntry(id)?.data; + private _agentIsEnabled(id: string): boolean { + const entry = this._agents.get(id); + return !entry?.data.when || this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(entry.data.when)); + } + + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { + const agent = Iterable.find(this._agents.values(), a => getFullyQualifiedId(a.data) === id)?.data; + if (agent && !this._agentIsEnabled(agent.id)) { + return; + } + + return agent; } /** * Returns all agent datas that exist- static registered and dynamic ones. */ getAgents(): IChatAgentData[] { - return this._agents.map(entry => entry.data); + return Array.from(this._agents.values()) + .map(entry => entry.data) + .filter(a => this._agentIsEnabled(a.id)); } getActivatedAgents(): IChatAgent[] { return Array.from(this._agents.values()) .filter(a => !!a.impl) + .filter(a => this._agentIsEnabled(a.data.id)) .map(a => new MergedChatAgent(a.data, a.impl!)); } @@ -253,19 +370,29 @@ export class ChatAgentService implements IChatAgentService { return this.getAgents().filter(a => a.name === name); } + agentHasDupeName(id: string): boolean { + const agent = this.getAgent(id); + if (!agent) { + return false; + } + + return this.getAgentsByName(agent.name) + .filter(a => a.extensionId.value !== agent.extensionId.value).length > 0; + } + async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._getAgentEntry(id); + const data = this._agents.get(id); if (!data?.impl) { - throw new Error(`No activated agent with id ${id}`); + throw new Error(`No activated agent with id "${id}"`); } return await data.impl.invoke(request, progress, history, token); } async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._getAgentEntry(id); + const data = this._agents.get(id); if (!data?.impl) { - throw new Error(`No activated agent with id ${id}`); + throw new Error(`No activated agent with id "${id}"`); } if (!data.impl?.provideFollowups) { @@ -274,6 +401,53 @@ export class ChatAgentService implements IChatAgentService { return data.impl.provideFollowups(request, result, history, token); } + + private _chatParticipantDetectionProviders = new Map(); + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider) { + this._chatParticipantDetectionProviders.set(handle, provider); + return toDisposable(() => { + this._chatParticipantDetectionProviders.delete(handle); + }); + } + + async detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { + // TODO@joyceerhl should we have a selector to be able to narrow down which provider to use + const provider = Iterable.first(this._chatParticipantDetectionProviders.values()); + if (!provider) { + return; + } + + const participants = this.getAgents().reduce((acc, a) => { + acc.push({ participant: a.id, description: undefined }); + for (const command of a.slashCommands) { + acc.push({ participant: a.id, command: command.name, description: undefined }); + } + return acc; + }, []); + + const result = await provider.provideParticipantDetection(request, history, { ...options, participants }, token); + if (!result) { + return; + } + + const agent = this.getAgent(result.participant); + if (!agent) { + // Couldn't find a participant matching the participant detection result + return; + } + + if (!result.command) { + return { agent }; + } + + const command = agent?.slashCommands.find(c => c.name === result.command); + if (!command) { + // Couldn't find a slash command matching the participant detection result + return; + } + + return { agent, command }; + } } export class MergedChatAgent implements IChatAgent { @@ -284,12 +458,15 @@ export class MergedChatAgent implements IChatAgent { get id(): string { return this.data.id; } get name(): string { return this.data.name ?? ''; } + get fullName(): string { return this.data.fullName ?? ''; } get description(): string { return this.data.description ?? ''; } get extensionId(): ExtensionIdentifier { return this.data.extensionId; } + get extensionPublisherId(): string { return this.data.extensionPublisherId; } + get extensionPublisherDisplayName() { return this.data.publisherDisplayName; } + get extensionDisplayName(): string { return this.data.extensionDisplayName; } get isDefault(): boolean | undefined { return this.data.isDefault; } get metadata(): IChatAgentMetadata { return this.data.metadata; } get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } - get defaultImplicitVariables(): string[] | undefined { return this.data.defaultImplicitVariables; } get locations(): ChatAgentLocation[] { return this.data.locations; } async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { @@ -304,19 +481,151 @@ export class MergedChatAgent implements IChatAgent { return []; } - provideWelcomeMessage(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { + provideWelcomeMessage(location: ChatAgentLocation, token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { if (this.impl.provideWelcomeMessage) { - return this.impl.provideWelcomeMessage(token); + return this.impl.provideWelcomeMessage(location, token); } return undefined; } - provideSampleQuestions(token: CancellationToken): ProviderResult { + provideSampleQuestions(location: ChatAgentLocation, token: CancellationToken): ProviderResult { if (this.impl.provideSampleQuestions) { - return this.impl.provideSampleQuestions(token); + return this.impl.provideSampleQuestions(location, token); } return undefined; } } + +export const IChatAgentNameService = createDecorator('chatAgentNameService'); + +type IChatParticipantRegistry = { [name: string]: string[] }; + +interface IChatParticipantRegistryResponse { + readonly version: number; + readonly restrictedChatParticipants: IChatParticipantRegistry; +} + +export interface IChatAgentNameService { + _serviceBrand: undefined; + getAgentNameRestriction(chatAgentData: IChatAgentData): boolean; +} + +export class ChatAgentNameService implements IChatAgentNameService { + + private static readonly StorageKey = 'chat.participantNameRegistry'; + + declare _serviceBrand: undefined; + + private readonly url!: string; + private registry = observableValue(this, Object.create(null)); + private disposed = false; + + constructor( + @IProductService productService: IProductService, + @IRequestService private readonly requestService: IRequestService, + @ILogService private readonly logService: ILogService, + @IStorageService private readonly storageService: IStorageService + ) { + if (!productService.chatParticipantRegistry) { + return; + } + + this.url = productService.chatParticipantRegistry; + + const raw = storageService.get(ChatAgentNameService.StorageKey, StorageScope.APPLICATION); + + try { + this.registry.set(JSON.parse(raw ?? '{}'), undefined); + } catch (err) { + storageService.remove(ChatAgentNameService.StorageKey, StorageScope.APPLICATION); + } + + this.refresh(); + } + + private refresh(): void { + if (this.disposed) { + return; + } + + this.update() + .catch(err => this.logService.warn('Failed to fetch chat participant registry', err)) + .then(() => timeout(5 * 60 * 1000)) // every 5 minutes + .then(() => this.refresh()); + } + + private async update(): Promise { + const context = await this.requestService.request({ type: 'GET', url: this.url }, CancellationToken.None); + + if (context.res.statusCode !== 200) { + throw new Error('Could not get extensions report.'); + } + + const result = await asJson(context); + + if (!result || result.version !== 1) { + throw new Error('Unexpected chat participant registry response.'); + } + + const registry = result.restrictedChatParticipants; + this.registry.set(registry, undefined); + this.storageService.store(ChatAgentNameService.StorageKey, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE); + } + + /** + * Returns true if the agent is allowed to use this name + */ + getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { + // TODO would like to use observables here but nothing uses it downstream and I'm not sure how to combine these two + const nameAllowed = this.checkAgentNameRestriction(chatAgentData.name, chatAgentData).get(); + const fullNameAllowed = !chatAgentData.fullName || this.checkAgentNameRestriction(chatAgentData.fullName.replace(/\s/g, ''), chatAgentData).get(); + return nameAllowed && fullNameAllowed; + } + + private checkAgentNameRestriction(name: string, chatAgentData: IChatAgentData): IObservable { + // Registry is a map of name to an array of extension publisher IDs or extension IDs that are allowed to use it. + // Look up the list of extensions that are allowed to use this name + const allowList = this.registry.map(registry => registry[name.toLowerCase()]); + return allowList.map(allowList => { + if (!allowList) { + return true; + } + + return allowList.some(id => equalsIgnoreCase(id, id.includes('.') ? chatAgentData.extensionId.value : chatAgentData.extensionPublisherId)); + }); + } + + dispose() { + this.disposed = true; + } +} + +export function getFullyQualifiedId(chatAgentData: IChatAgentData): string { + return `${chatAgentData.extensionId.value}.${chatAgentData.id}`; +} + +export function reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { + const agent = 'name' in raw ? + raw : + { + ...(raw as any), + name: (raw as any).id, + }; + + // Fill in required fields that may be missing from old data + if (!('extensionPublisherId' in agent)) { + agent.extensionPublisherId = agent.extensionPublisher ?? ''; + } + + if (!('extensionDisplayName' in agent)) { + agent.extensionDisplayName = ''; + } + + if (!('extensionId' in agent)) { + agent.extensionId = new ExtensionIdentifier(''); + } + + return revive(agent); +} diff --git a/src/vs/workbench/contrib/chat/common/chatColors.ts b/src/vs/workbench/contrib/chat/common/chatColors.ts index 385c0cf20c4..15451f0de58 100644 --- a/src/vs/workbench/contrib/chat/common/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/chatColors.ts @@ -21,7 +21,7 @@ export const chatRequestBackground = registerColor( export const chatSlashCommandBackground = registerColor( 'chat.slashCommandBackground', - { dark: '#34414B', light: '#D2ECFF', hcDark: Color.white, hcLight: badgeBackground }, + { dark: '#34414b8f', light: '#d2ecff99', hcDark: Color.white, hcLight: badgeBackground }, localize('chat.slashCommandBackground', 'The background color of a chat slash command.') ); @@ -39,6 +39,6 @@ export const chatAvatarBackground = registerColor( export const chatAvatarForeground = registerColor( 'chat.avatarForeground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground, }, + foreground, localize('chat.avatarForeground', 'The foreground color of a chat avatar.') ); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 417b72ff410..7a17a0dfdf5 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -8,6 +8,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; export const CONTEXT_RESPONSE_VOTE = 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.") }); +export const CONTEXT_VOTE_UP_ENABLED = new RawContextKey('chatVoteUpEnabled', false, { type: 'boolean', description: localize('chatVoteUpEnabled', "True when the chat vote up action is enabled.") }); export const CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND = new RawContextKey('chatSessionResponseDetectedAgentOrCommand', false, { type: 'boolean', description: localize('chatSessionResponseDetectedAgentOrCommand', "When the agent or command was automatically detected") }); export const CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING = new RawContextKey('chatResponseSupportsIssueReporting', false, { type: 'boolean', description: localize('chatResponseSupportsIssueReporting', "True when the current chat response supports issue reporting.") }); export const CONTEXT_RESPONSE_FILTERED = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); @@ -16,12 +17,15 @@ export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('chat export const CONTEXT_RESPONSE = new RawContextKey('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") }); export const CONTEXT_REQUEST = new RawContextKey('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") }); +export const CONTEXT_CHAT_EDIT_APPLIED = new RawContextKey('chatEditApplied', false, { type: 'boolean', description: localize('chatEditApplied', "True when the chat text edits have been applied.") }); + export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") }); export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); -export const CONTEXT_PROVIDER_EXISTS = new RawContextKey('hasChatProvider', false, { type: 'boolean', description: localize('hasChatProvider', "True when some chat provider has been registered.") }); +export const CONTEXT_CHAT_ENABLED = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is registered.") }); export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey('chatCursorAtTop', false); export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey('chatInputHasAgent', false); export const CONTEXT_CHAT_LOCATION = new RawContextKey('chatLocation', undefined); +export const CONTEXT_IN_QUICK_CHAT = new RawContextKey('quickChatHasFocus', false, { type: 'boolean', description: localize('inQuickChat', "True when the quick chat UI has focus, false otherwise.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 84913ffc2c2..c85baeb5551 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -8,30 +8,36 @@ import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; import { revive } from 'vs/base/common/marshalling'; -import { basename } from 'vs/base/common/resources'; +import { equals } from 'vs/base/common/objects'; +import { basename, isEqual } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI, UriComponents, UriDto, isUriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { TextEdit } from 'vs/editor/common/languages'; +import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; -export interface IChatPromptVariableData { - variables: { name: string; range: IOffsetRange; values: IChatRequestVariableValue[] }[]; -} - export interface IChatRequestVariableEntry { + id: string; + fullName?: string; + icon?: ThemeIcon; name: string; + modelDescription?: string; range?: IOffsetRange; - values: IChatRequestVariableValue[]; + value: IChatRequestVariableValue; references?: IChatContentReference[]; + + // TODO are these just a 'kind'? + isDynamic?: boolean; + isFile?: boolean; + isTool?: boolean; } export interface IChatRequestVariableData { @@ -44,29 +50,47 @@ export interface IChatRequestModel { readonly avatarIconUri?: URI; readonly session: IChatModel; readonly message: IParsedChatRequest; + readonly attempt: number; readonly variableData: IChatRequestVariableData; + readonly confirmation?: string; readonly response?: IChatResponseModel; } +export interface IChatTextEditGroupState { + sha1: string; + applied: number; +} + +export interface IChatTextEditGroup { + uri: URI; + edits: TextEdit[][]; + state?: IChatTextEditGroupState; + kind: 'textEditGroup'; +} + export type IChatProgressResponseContent = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability | IChatTreeData | IChatContentInlineReference | IChatProgressMessage - | IChatCommandButton; + | IChatCommandButton + | IChatWarningMessage + | IChatTask + | IChatTextEditGroup + | IChatConfirmation; export type IChatProgressRenderableResponseContent = Exclude; export interface IResponse { readonly value: ReadonlyArray; - asString(): string; + toMarkdown(): string; + toString(): string; } export interface IChatResponseModel { readonly onDidChange: Event; readonly id: string; - readonly providerId: string; readonly requestId: string; readonly username: string; readonly avatarIcon?: ThemeIcon | URI; @@ -74,19 +98,20 @@ export interface IChatResponseModel { readonly agent?: IChatAgentData; readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; + readonly codeCitations: ReadonlyArray; readonly progressMessages: ReadonlyArray; readonly slashCommand?: IChatAgentCommand; readonly agentOrSlashCommandDetected: boolean; readonly response: IResponse; - readonly edits: ResourceMap; readonly isComplete: boolean; readonly isCanceled: boolean; /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ readonly isStale: boolean; - readonly vote: InteractiveSessionVoteDirection | undefined; + readonly vote: ChatAgentVoteDirection | undefined; readonly followups?: IChatFollowup[] | undefined; readonly result?: IChatAgentResult; - setVote(vote: InteractiveSessionVoteDirection): void; + setVote(vote: ChatAgentVoteDirection): void; + setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean; } export class ChatRequestModel implements IChatRequestModel { @@ -94,9 +119,10 @@ export class ChatRequestModel implements IChatRequestModel { public response: ChatResponseModel | undefined; - private _id: string; - public get id(): string { - return this._id; + public readonly id: string; + + public get session() { + return this._session; } public get username(): string { @@ -107,6 +133,10 @@ export class ChatRequestModel implements IChatRequestModel { return this.session.requesterAvatarIconUri; } + public get attempt(): number { + return this._attempt; + } + public get variableData(): IChatRequestVariableData { return this._variableData; } @@ -115,30 +145,51 @@ export class ChatRequestModel implements IChatRequestModel { this._variableData = v; } + public get confirmation(): string | undefined { + return this._confirmation; + } + constructor( - public readonly session: ChatModel, + private _session: ChatModel, public readonly message: IParsedChatRequest, - private _variableData: IChatRequestVariableData) { - this._id = 'request_' + ChatRequestModel.nextId++; + private _variableData: IChatRequestVariableData, + private _attempt: number = 0, + private _confirmation?: string + ) { + this.id = 'request_' + ChatRequestModel.nextId++; + } + + adoptTo(session: ChatModel) { + this._session = session; } } -export class Response implements IResponse { - private _onDidChangeValue = new Emitter(); +export class Response extends Disposable implements IResponse { + private _onDidChangeValue = this._register(new Emitter()); public get onDidChangeValue() { return this._onDidChangeValue.event; } - // responseParts internally tracks all the response parts, including strings which are currently resolving, so that they can be updated when they do resolve private _responseParts: IChatProgressResponseContent[]; - // responseRepr externally presents the response parts with consolidated contiguous strings (excluding tree data) - private _responseRepr!: string; + + /** + * A stringified representation of response data which might be presented to a screenreader or used when copying a response. + */ + private _responseRepr = ''; + + /** + * Just the markdown content of the response, used for determining the rendering rate of markdown + */ + private _markdownContent = ''; + + private _citations: IChatCodeCitation[] = []; get value(): IChatProgressResponseContent[] { return this._responseParts; } constructor(value: IMarkdownString | ReadonlyArray) { + super(); this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : 'kind' in v ? v : { kind: 'treeData', treeData: v })); @@ -146,48 +197,82 @@ export class Response implements IResponse { this._updateRepr(true); } - asString(): string { + override toString(): string { return this._responseRepr; } + toMarkdown(): string { + return this._markdownContent; + } + clear(): void { this._responseParts = []; this._updateRepr(true); } - updateContent(progress: IChatProgressResponseContent | IChatContent, quiet?: boolean): void { - if (progress.kind === 'content' || progress.kind === 'markdownContent') { + updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatTask, quiet?: boolean): void { + if (progress.kind === 'markdownContent') { const responsePartLength = this._responseParts.length - 1; const lastResponsePart = this._responseParts[responsePartLength]; - if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent') { - // The last part can't be merged with - if (progress.kind === 'content') { - this._responseParts.push({ content: new MarkdownString(progress.content), kind: 'markdownContent' }); - } else { - this._responseParts.push(progress); - } - } else if (progress.kind === 'markdownContent') { - // Merge all enabled commands - const lastPartEnabledCommands = typeof lastResponsePart.content.isTrusted === 'object' ? - lastResponsePart.content.isTrusted.enabledCommands : - []; - const thisPartEnabledCommands = typeof progress.content.isTrusted === 'object' ? - progress.content.isTrusted.enabledCommands : - []; - const enabledCommands = [...lastPartEnabledCommands, ...thisPartEnabledCommands]; - this._responseParts[responsePartLength] = { content: new MarkdownString(lastResponsePart.content.value + progress.content.value, { isTrusted: { enabledCommands } }), kind: 'markdownContent' }; + if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) { + // The last part can't be merged with- not markdown, or markdown with different permissions + this._responseParts.push(progress); } else { - this._responseParts[responsePartLength] = { content: new MarkdownString(lastResponsePart.content.value + progress.content, lastResponsePart.content), kind: 'markdownContent' }; + lastResponsePart.content = appendMarkdownString(lastResponsePart.content, progress.content); } - this._updateRepr(quiet); + } else if (progress.kind === 'textEdit') { + if (progress.edits.length > 0) { + // merge text edits for the same file no matter when they come in + let found = false; + for (let i = 0; !found && i < this._responseParts.length; i++) { + const candidate = this._responseParts[i]; + if (candidate.kind === 'textEditGroup' && isEqual(candidate.uri, progress.uri)) { + candidate.edits.push(progress.edits); + found = true; + } + } + if (!found) { + this._responseParts.push({ + kind: 'textEditGroup', + uri: progress.uri, + edits: [progress.edits] + }); + } + this._updateRepr(quiet); + } + } else if (progress.kind === 'progressTask') { + // Add a new resolving part + const responsePosition = this._responseParts.push(progress) - 1; + this._updateRepr(quiet); + + const disp = progress.onDidAddProgress(() => { + this._updateRepr(false); + }); + + progress.task?.().then((content) => { + // Stop listening for progress updates once the task settles + disp.dispose(); + + // Replace the resolving part's content with the resolved response + if (typeof content === 'string') { + (this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content); + } + this._updateRepr(false); + }); + } else { this._responseParts.push(progress); this._updateRepr(quiet); } } + public addCitation(citation: IChatCodeCitation) { + this._citations.push(citation); + this._updateRepr(); + } + private _updateRepr(quiet?: boolean) { this._responseRepr = this._responseParts.map(part => { if (part.kind === 'treeData') { @@ -196,10 +281,32 @@ export class Response implements IResponse { return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); } else if (part.kind === 'command') { return part.command.title; + } else if (part.kind === 'textEditGroup') { + return localize('editsSummary', "Made changes."); + } else if (part.kind === 'progressMessage') { + return ''; + } else if (part.kind === 'confirmation') { + return `${part.title}\n${part.message}`; } else { return part.content.value; } - }).join('\n\n'); + }) + .filter(s => s.length > 0) + .join('\n\n'); + + this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : ''; + + this._markdownContent = this._responseParts.map(part => { + if (part.kind === 'inlineReference') { + return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); + } else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { + return part.content.value; + } else { + return ''; + } + }) + .filter(s => s.length > 0) + .join('\n\n'); if (!quiet) { this._onDidChangeValue.fire(); @@ -213,9 +320,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel private static nextId = 0; - private _id: string; - public get id(): string { - return this._id; + public readonly id: string; + + public get session() { + return this._session; } public get isComplete(): boolean { @@ -226,7 +334,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._isCanceled; } - public get vote(): InteractiveSessionVoteDirection | undefined { + public get vote(): ChatAgentVoteDirection | undefined { return this._vote; } @@ -239,19 +347,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._response; } - private _edits: ResourceMap; - public get edits(): ResourceMap { - return this._edits; - } - public get result(): IChatAgentResult | undefined { return this._result; } - public get providerId(): string { - return this.session.providerId; - } - public get username(): string { return this.session.responderUsername; } @@ -285,6 +384,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._contentReferences; } + private readonly _codeCitations: IChatCodeCitation[] = []; + public get codeCitations(): ReadonlyArray { + return this._codeCitations; + } + private readonly _progressMessages: IChatProgressMessage[] = []; public get progressMessages(): ReadonlyArray { return this._progressMessages; @@ -297,13 +401,13 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel constructor( _response: IMarkdownString | ReadonlyArray, - public readonly session: ChatModel, + private _session: ChatModel, private _agent: IChatAgentData | undefined, private _slashCommand: IChatAgentCommand | undefined, public readonly requestId: string, private _isComplete: boolean = false, private _isCanceled = false, - private _vote?: InteractiveSessionVoteDirection, + private _vote?: ChatAgentVoteDirection, private _result?: IChatAgentResult, followups?: ReadonlyArray ) { @@ -313,29 +417,18 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._isStale = Array.isArray(_response) && (_response.length !== 0 || isMarkdownString(_response) && _response.value.length !== 0); this._followups = followups ? [...followups] : undefined; - this._response = new Response(_response); - this._edits = new ResourceMap(); + this._response = this._register(new Response(_response)); this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); - this._id = 'response_' + ChatResponseModel.nextId++; + this.id = 'response_' + ChatResponseModel.nextId++; } /** * Apply a progress update to the actual response content. */ - updateContent(responsePart: IChatProgressResponseContent | IChatContent, quiet?: boolean) { + updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit, quiet?: boolean) { this._response.updateContent(responsePart, quiet); } - updateTextEdits(uri: URI, edits: TextEdit[]) { - const array = this._edits.get(uri); - if (!array) { - this._edits.set(uri, edits); - } else { - array.push(...edits); - } - this._onDidChange.fire(); - } - /** * Apply one of the progress updates that are not part of the actual response content. */ @@ -348,6 +441,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } } + applyCodeCitation(progress: IChatCodeCitation) { + this._codeCitations.push(progress); + this._response.addCitation(progress); + this._onDidChange.fire(); + } + setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) { this._agent = agent; this._slashCommand = slashCommand; @@ -380,18 +479,35 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._onDidChange.fire(); // Fire so that command followups get rendered on the row } - setVote(vote: InteractiveSessionVoteDirection): void { + setVote(vote: ChatAgentVoteDirection): void { this._vote = vote; this._onDidChange.fire(); } + + setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean { + if (!this.response.value.includes(edit)) { + return false; + } + if (!edit.state) { + return false; + } + edit.state.applied = editCount; // must not be edit.edits.length + this._onDidChange.fire(); + return true; + } + + adoptTo(session: ChatModel) { + this._session = session; + this._onDidChange.fire(); + } } export interface IChatModel { readonly onDidDispose: Event; readonly onDidChange: Event; readonly sessionId: string; - readonly providerId: string; readonly initState: ChatModelInitState; + readonly initialLocation: ChatAgentLocation; readonly title: string; readonly welcomeMessage: IChatWelcomeMessageModel | undefined; readonly requestInProgress: boolean; @@ -418,14 +534,15 @@ export interface ISerializableChatRequestData { result?: IChatAgentResult; // Optional for backcompat followups: ReadonlyArray | undefined; isCanceled: boolean | undefined; - vote: InteractiveSessionVoteDirection | undefined; + vote: ChatAgentVoteDirection | undefined; /** For backward compat: should be optional */ usedContext?: IChatUsedContext; contentReferences?: ReadonlyArray; + codeCitations?: ReadonlyArray; } export interface IExportableChatData { - providerId: string; + initialLocation: ChatAgentLocation | undefined; welcomeMessage: (string | IChatFollowup[])[] | undefined; requests: ISerializableChatRequestData[]; requesterUsername: string; @@ -438,12 +555,14 @@ export interface ISerializableChatData extends IExportableChatData { sessionId: string; creationDate: number; isImported: boolean; + + /** Indicates that this session was created in this window. Is cleared after the chat has been written to storage once. Needed to sync chat creations/deletions between empty windows. */ + isNew?: boolean; } export function isExportableSessionData(obj: unknown): obj is IExportableChatData { const data = obj as IExportableChatData; return typeof data === 'object' && - typeof data.providerId === 'string' && typeof data.requesterUsername === 'string'; } @@ -457,22 +576,55 @@ export function isSerializableSessionData(obj: unknown): obj is ISerializableCha ); } -export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent | IChatRemoveRequestEvent; +export type IChatChangeEvent = + | IChatInitEvent + | IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent + | IChatAddResponseEvent + | IChatSetAgentEvent; export interface IChatAddRequestEvent { kind: 'addRequest'; request: IChatRequestModel; } +export interface IChatChangedRequestEvent { + kind: 'changedRequest'; + request: IChatRequestModel; +} + export interface IChatAddResponseEvent { kind: 'addResponse'; response: IChatResponseModel; } +export const enum ChatRequestRemovalReason { + /** + * "Normal" remove + */ + Removal, + + /** + * Removed because the request will be resent + */ + Resend, + + /** + * Remove because the request is moving to another model + */ + Adoption +} + export interface IChatRemoveRequestEvent { kind: 'removeRequest'; requestId: string; responseId?: string; + reason: ChatRequestRemovalReason; +} + +export interface IChatSetAgentEvent { + kind: 'setAgent'; + agent: IChatAgentData; + command?: IChatAgentCommand; } export interface IChatInitEvent { @@ -504,11 +656,6 @@ export class ChatModel extends Disposable implements IChatModel { private _initState: ChatModelInitState = ChatModelInitState.Created; private _isInitializedDeferred = new DeferredPromise(); - private _session: IChat | undefined; - get session(): IChat | undefined { - return this._session; - } - private _welcomeMessage: ChatWelcomeMessageModel | undefined; get welcomeMessage(): ChatWelcomeMessageModel | undefined { return this._welcomeMessage; @@ -526,6 +673,10 @@ export class ChatModel extends Disposable implements IChatModel { return !!lastRequest && !!lastRequest.response && !lastRequest.response.isComplete; } + get hasRequests(): boolean { + return this._requests.length > 0; + } + private _creationDate: number; get creationDate(): number { return this._creationDate; @@ -543,7 +694,7 @@ export class ChatModel extends Disposable implements IChatModel { get responderUsername(): string { return (this._defaultAgent ? - this._defaultAgent.metadata.fullName : + this._defaultAgent.fullName : this.initialData?.responderUsername) ?? ''; } @@ -574,9 +725,13 @@ export class ChatModel extends Disposable implements IChatModel { return ChatModel.getDefaultTitle(this._requests); } + get initialLocation() { + return this._initialLocation; + } + constructor( - public readonly providerId: string, private readonly initialData: ISerializableChatData | IExportableChatData | undefined, + private readonly _initialLocation: ChatAgentLocation, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -612,25 +767,23 @@ export class ChatModel extends Disposable implements IChatModel { : reviveParsedChatRequest(raw.message); // Old messages don't have variableData, or have it in the wrong (non-array) shape - const variableData: IChatRequestVariableData = raw.variableData && Array.isArray(raw.variableData.variables) - ? raw.variableData : - { variables: [] }; + const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); const request = new ChatRequestModel(this, parsedRequest, variableData); if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format - this.reviveSerializedAgent(raw.agent) : undefined; + reviveSerializedAgent(raw.agent) : undefined; // Port entries from old format const result = 'responseErrorDetails' in raw ? + // eslint-disable-next-line local/code-no-dangerous-type-assertions { errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result; request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, result, raw.followups); if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? request.response.applyReference(revive(raw.usedContext)); } - if (raw.contentReferences) { - raw.contentReferences.forEach(r => request.response!.applyReference(revive(r))); - } + raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r))); + raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c))); } return request; }); @@ -640,14 +793,28 @@ export class ChatModel extends Disposable implements IChatModel { } } - private reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { - const agent = 'name' in raw ? - raw : - { - ...(raw as any), - name: (raw as any).id, - }; - return revive(agent); + private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData { + const variableData = raw && Array.isArray(raw.variables) + ? raw : + { variables: [] }; + + variableData.variables = variableData.variables.map((v): IChatRequestVariableEntry => { + // Old variables format + if (v && 'values' in v && Array.isArray(v.values)) { + return { + id: v.id ?? '', + name: v.name, + value: v.values[0]?.value, + range: v.range, + modelDescription: v.modelDescription, + references: v.references + }; + } else { + return v; + } + }); + + return variableData; } private getParsedRequestFromString(message: string): IParsedChatRequest { @@ -667,19 +834,17 @@ export class ChatModel extends Disposable implements IChatModel { } deinitialize(): void { - this._session = undefined; this._initState = ChatModelInitState.Created; this._isInitializedDeferred = new DeferredPromise(); } - initialize(session: IChat, welcomeMessage: ChatWelcomeMessageModel | undefined): void { + initialize(welcomeMessage: ChatWelcomeMessageModel | undefined): 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._session = session; if (!this._welcomeMessage) { // Could also have loaded the welcome message from persisted data this._welcomeMessage = welcomeMessage; @@ -707,12 +872,8 @@ export class ChatModel extends Disposable implements IChatModel { return this._requests; } - addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand): ChatRequestModel { - if (!this._session) { - throw new Error('addRequest: No session'); - } - - const request = new ChatRequestModel(this, message, variableData); + addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string): ChatRequestModel { + const request = new ChatRequestModel(this, message, variableData, attempt, confirmation); request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id); this._requests.push(request); @@ -720,11 +881,32 @@ export class ChatModel extends Disposable implements IChatModel { return request; } - acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void { - if (!this._session) { - throw new Error('acceptResponseProgress: No session'); + updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) { + request.variableData = variableData; + this._onDidChange.fire({ kind: 'changedRequest', request }); + } + + adoptRequest(request: ChatRequestModel): void { + + // this doesn't use `removeRequest` because it must not dispose the request object + const oldOwner = request.session; + const index = oldOwner._requests.findIndex(candidate => candidate.id === request.id); + + if (index === -1) { + return; } + oldOwner._requests.splice(index, 1); + + request.adoptTo(this); + request.response?.adoptTo(this); + this._requests.push(request); + + oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption }); + this._onDidChange.fire({ kind: 'addRequest', request }); + } + + acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void { if (!request.response) { request.response = new ChatResponseModel([], this, undefined, undefined, request.id); } @@ -733,9 +915,17 @@ export class ChatModel extends Disposable implements IChatModel { throw new Error('acceptResponseProgress: Adding progress to a completed response'); } - if (progress.kind === 'vulnerability') { - request.response.updateContent({ kind: 'markdownVuln', content: { value: progress.content }, vulnerabilities: progress.vulnerabilities }, quiet); - } else if (progress.kind === 'content' || progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command') { + if (progress.kind === 'markdownContent' || + progress.kind === 'treeData' || + progress.kind === 'inlineReference' || + progress.kind === 'markdownVuln' || + progress.kind === 'progressMessage' || + progress.kind === 'command' || + progress.kind === 'textEdit' || + progress.kind === 'warning' || + progress.kind === 'progressTask' || + progress.kind === 'confirmation' + ) { request.response.updateContent(progress, quiet); } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); @@ -743,20 +933,21 @@ export class ChatModel extends Disposable implements IChatModel { const agent = this.chatAgentService.getAgent(progress.agentId); if (agent) { request.response.setAgent(agent, progress.command); + this._onDidChange.fire({ kind: 'setAgent', agent, command: progress.command }); } - } else if (progress.kind === 'textEdit') { - request.response.updateTextEdits(progress.uri, progress.edits); + } else if (progress.kind === 'codeCitation') { + request.response.applyCodeCitation(progress); } else { this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); } } - removeRequest(id: string): void { + removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void { const index = this._requests.findIndex(request => request.id === id); const request = this._requests[index]; if (index !== -1) { - this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id }); + this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason }); this._requests.splice(index, 1); request.response?.dispose(); } @@ -769,10 +960,6 @@ export class ChatModel extends Disposable implements IChatModel { } setResponse(request: ChatRequestModel, result: IChatAgentResult): void { - if (!this._session) { - throw new Error('completeResponse: No session'); - } - if (!request.response) { request.response = new ChatResponseModel([], this, undefined, undefined, request.id); } @@ -808,6 +995,7 @@ export class ChatModel extends Disposable implements IChatModel { requesterAvatarIconUri: this.requesterAvatarIconUri, responderUsername: this.responderUsername, responderAvatarIconUri: this.responderAvatarIcon, + initialLocation: this.initialLocation, welcomeMessage: this._welcomeMessage?.content.map(c => { if (Array.isArray(c)) { return c; @@ -839,16 +1027,13 @@ export class ChatModel extends Disposable implements IChatModel { followups: r.response?.followups, isCanceled: r.response?.isCanceled, vote: r.response?.vote, - agent: r.response?.agent ? - // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. - { id: r.response.agent.id, name: r.response.agent.name, description: r.response.agent.description, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault } - : undefined, + agent: r.response?.agent ? { ...r.response.agent } : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, - contentReferences: r.response?.contentReferences + contentReferences: r.response?.contentReferences, + codeCitations: r.response?.codeCitations }; }), - providerId: this.providerId, }; } @@ -862,7 +1047,6 @@ export class ChatModel extends Disposable implements IChatModel { } override dispose() { - this._session?.dispose?.(); this._requests.forEach(r => r.response?.dispose()); this._onDidDispose.fire(); @@ -898,7 +1082,7 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } public get username(): string { - return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.metadata.fullName ?? ''; + return this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel)?.fullName ?? ''; } public get avatarIcon(): ThemeIcon | undefined { @@ -906,13 +1090,19 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } } -export function getHistoryEntriesFromModel(model: IChatModel): IChatAgentHistoryEntry[] { +export function getHistoryEntriesFromModel(model: IChatModel, forAgentId: string | undefined): IChatAgentHistoryEntry[] { const history: IChatAgentHistoryEntry[] = []; for (const request of model.getRequests()) { if (!request.response) { continue; } + if (forAgentId && forAgentId !== request.response.agent?.id) { + // 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; + } + const promptTextResult = getPromptText(request.message); const historyRequest: IChatAgentRequest = { sessionId: model.sessionId, @@ -940,3 +1130,45 @@ export function updateRanges(variableData: IChatRequestVariableData, diff: numbe })) }; } + +export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean { + if (md1.baseUri && md2.baseUri) { + const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme + && md1.baseUri.authority === md2.baseUri.authority + && md1.baseUri.path === md2.baseUri.path + && md1.baseUri.query === md2.baseUri.query + && md1.baseUri.fragment === md2.baseUri.fragment; + if (!baseUriEquals) { + return false; + } + } else if (md1.baseUri || md2.baseUri) { + return false; + } + + return equals(md1.isTrusted, md2.isTrusted) && + md1.supportHtml === md2.supportHtml && + md1.supportThemeIcons === md2.supportThemeIcons; +} + +export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString { + const appendedValue = typeof md2 === 'string' ? md2 : md2.value; + return { + value: md1.value + appendedValue, + isTrusted: md1.isTrusted, + supportThemeIcons: md1.supportThemeIcons, + supportHtml: md1.supportHtml, + baseUri: md1.baseUri + }; +} + +export function getCodeCitationsMessage(citations: ReadonlyArray): string { + if (citations.length === 0) { + return ''; + } + + const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set()); + const label = licenseTypes.size === 1 ? + localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) : + localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size); + return label; +} diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 7edda68e10e..67f11571229 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -54,7 +54,7 @@ export const chatSubcommandLeader = '/'; export class ChatRequestVariablePart implements IParsedChatRequestPart { static readonly Kind = 'var'; readonly kind = ChatRequestVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string, readonly variableId: string) { } get text(): string { const argPart = this.variableArg ? `:${this.variableArg}` : ''; @@ -81,23 +81,6 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart { get promptText(): string { return ''; } - - /** - * Don't stringify all the agent methods, just data. - */ - toJSON(): any { - return { - kind: this.kind, - range: this.range, - editorRange: this.editorRange, - agent: { - id: this.agent.id, - name: this.agent.name, - description: this.agent.description, - metadata: this.agent.metadata - } - }; - } } /** @@ -140,7 +123,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 data: IChatRequestVariableValue[]) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IChatRequestVariableValue) { } get referenceText(): string { return this.text.replace(chatVariableLeader, ''); @@ -166,17 +149,12 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, (part as ChatRequestVariablePart).variableName, - (part as ChatRequestVariablePart).variableArg + (part as ChatRequestVariablePart).variableArg, + (part as ChatRequestVariablePart).variableName || '', ); } else if (part.kind === ChatRequestAgentPart.Kind) { let agent = (part as ChatRequestAgentPart).agent; - if (!('name' in agent)) { - // Port old format - agent = { - ...(agent as any), - name: (agent as any).id - }; - } + agent = reviveSerializedAgent(agent); return new ChatRequestAgentPart( new OffsetRange(part.range.start, part.range.endExclusive), @@ -200,6 +178,8 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, (part as ChatRequestDynamicVariablePart).text, + (part as ChatRequestDynamicVariablePart).id, + (part as ChatRequestDynamicVariablePart).modelDescription, revive((part as ChatRequestDynamicVariablePart).data) ); } else { @@ -214,3 +194,20 @@ export function extractAgentAndCommand(parsed: IParsedChatRequest): { agentPart: const commandPart = parsed.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); return { agentPart, commandPart }; } + +export function formatChatQuestion(chatAgentService: IChatAgentService, location: ChatAgentLocation, prompt: string, participant: string | null = null, command: string | null = null): string | undefined { + let question = ''; + if (participant && participant !== chatAgentService.getDefaultAgent(location)?.id) { + const agent = chatAgentService.getAgent(participant); + if (!agent) { + // Refers to agent that doesn't exist + return undefined; + } + + question += `${chatAgentLeader}${agent.name} `; + if (command) { + question += `${chatSubcommandLeader}${command} `; + } + } + return question + prompt; +} diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts similarity index 50% rename from src/vs/workbench/contrib/chat/common/chatContributionService.ts rename to src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index 7d7452b1957..638dce2633c 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -3,31 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export interface IChatProviderContribution { - id: string; - when?: string; -} - -export const IChatContributionService = createDecorator('IChatContributionService'); -export interface IChatContributionService { - _serviceBrand: undefined; - - registeredProviders: IChatProviderContribution[]; - registerChatProvider(provider: IChatProviderContribution): void; - deregisterChatProvider(providerId: string): void; - getViewIdForProvider(providerId: string): string; -} - -export interface IRawChatProviderContribution { - id: string; - label: string; - icon?: string; - when?: string; -} - export interface IRawChatCommandContribution { name: string; description: string; @@ -42,15 +17,20 @@ export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook'; export interface IRawChatParticipantContribution { id: string; name: string; + fullName: string; + when?: string; description?: string; isDefault?: boolean; isSticky?: boolean; + sampleRequest?: string; commands?: IRawChatCommandContribution[]; defaultImplicitVariables?: string[]; locations?: RawChatParticipantLocation[]; } -export interface IChatParticipantContribution extends IRawChatParticipantContribution { - // Participant id is extensionId + name - extensionId: ExtensionIdentifier; -} +/** + * Hardcoding the previous id of the Copilot Chat provider to avoid breaking view locations, persisted data, etc. + * DON'T use this for any new data, only for old persisted data. + * @deprecated + */ +export const CHAT_PROVIDER_ID = 'copilot'; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index f60b9cb5dc3..d07026f5b7f 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -11,7 +11,7 @@ import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynami import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; -const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent +const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command @@ -43,7 +43,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); + newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts, location); } if (!newPart) { @@ -90,7 +90,7 @@ export class ChatRequestParser { }; } - private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined { + private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: Array, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined { const nextAgentMatch = message.match(agentReg); if (!nextAgentMatch) { return; @@ -100,14 +100,20 @@ export class ChatRequestParser { const agentRange = new OffsetRange(offset, offset + full.length); const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const agents = this.agentService.getAgentsByName(name); + let agents = this.agentService.getAgentsByName(name); + if (!agents.length) { + const fqAgent = this.agentService.getAgentByFullyQualifiedId(name); + if (fqAgent) { + agents = [fqAgent]; + } + } // If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the - // context and we use that one. Otherwise just pick the first. + // context and we use that one. const agent = agents.length > 1 && context?.selectedAgent ? context.selectedAgent : - agents[0]; - if (!agent || !agent.locations.includes(location)) { + agents.find((a) => a.locations.includes(location)); + if (!agent) { return; } @@ -141,15 +147,19 @@ export class ChatRequestParser { const variableArg = nextVariableMatch[2] ?? ''; const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + const allowSlow = !usedAgent || usedAgent.agent.metadata.supportsSlowVariables; - if (this.variableService.hasVariable(name)) { - return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg); + // TODO - not really handling duplicate variables names yet + const variable = this.variableService.getVariable(name); + if (variable && (!variable.isSlow || allowSlow)) { + return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg, variable.id); } return; } - private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; @@ -184,11 +194,19 @@ export class ChatRequestParser { return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); } } else { - const slashCommands = this.slashCommandService.getCommands(); + const slashCommands = this.slashCommandService.getCommands(location); const slashCommand = slashCommands.find(c => c.command === command); if (slashCommand) { // Valid standalone slash command return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand); + } else { + // check for with default agent for this location + const defaultAgent = this.agentService.getDefaultAgent(location); + const subCommand = defaultAgent?.slashCommands.find(c => c.name === command); + if (subCommand) { + // Valid default agent subcommand + return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); + } } } @@ -203,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.data); + return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data); } return; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 819c41f3045..65653fe84ce 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -3,28 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { Command, Location, ProviderResult, TextEdit } from 'vs/editor/common/languages'; +import { ISelection } from 'vs/editor/common/core/selection'; +import { Command, Location, TextEdit } from 'vs/editor/common/languages'; import { FileType } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; -export interface IChat { - id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about - dispose?(): void; -} - export interface IChatRequest { - session: IChat; message: string; variables: Record; } @@ -79,11 +75,26 @@ export interface IChatContentVariableReference { value?: URI | Location; } +export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 +} + export interface IChatContentReference { reference: URI | Location | IChatContentVariableReference; + iconPath?: ThemeIcon | { light: URI; dark?: URI }; + options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }; kind: 'reference'; } +export interface IChatCodeCitation { + value: URI; + license: string; + snippet: string; + kind: 'codeCitation'; +} + export interface IChatContentInlineReference { inlineReference: URI | Location; name?: string; @@ -96,11 +107,6 @@ export interface IChatAgentDetection { kind: 'agentDetection'; } -export interface IChatContent { - content: string; - kind: 'content'; -} - export interface IChatMarkdownContent { content: IMarkdownString; kind: 'markdownContent'; @@ -116,21 +122,40 @@ export interface IChatProgressMessage { kind: 'progressMessage'; } +export interface IChatTask extends IChatTaskDto { + deferred: DeferredPromise; + progress: (IChatWarningMessage | IChatContentReference)[]; + onDidAddProgress: Event; + add(progress: IChatWarningMessage | IChatContentReference): void; + + complete: (result: string | void) => void; + task: () => Promise; + isSettled: () => boolean; +} + +export interface IChatTaskDto { + content: IMarkdownString; + kind: 'progressTask'; +} + +export interface IChatTaskResult { + content: IMarkdownString | void; + kind: 'progressTaskResult'; +} + +export interface IChatWarningMessage { + content: IMarkdownString; + kind: 'warning'; +} + export interface IChatAgentVulnerabilityDetails { title: string; description: string; } -export interface IChatAgentContentWithVulnerabilities { - content: string; - vulnerabilities?: IChatAgentVulnerabilityDetails[]; - kind: 'vulnerability'; -} - -// TODO@roblourens Temp until I get MarkdownString out of ChatModel export interface IChatAgentMarkdownContentWithVulnerability { content: IMarkdownString; - vulnerabilities?: IChatAgentVulnerabilityDetails[]; + vulnerabilities: IChatAgentVulnerabilityDetails[]; kind: 'markdownVuln'; } @@ -145,24 +170,31 @@ export interface IChatTextEdit { kind: 'textEdit'; } +export interface IChatConfirmation { + title: string; + message: string; + data: any; + buttons?: string[]; + isUsed?: boolean; + kind: 'confirmation'; +} + export type IChatProgress = - | IChatContent | IChatMarkdownContent - | IChatAgentContentWithVulnerabilities | IChatAgentMarkdownContentWithVulnerability | IChatTreeData | IChatUsedContext | IChatContentReference | IChatContentInlineReference + | IChatCodeCitation | IChatAgentDetection | IChatProgressMessage + | IChatTask + | IChatTaskResult | IChatCommandButton - | IChatTextEdit; - -export interface IChatProvider { - readonly id: string; - prepareSession(token: CancellationToken): ProviderResult; -} + | IChatWarningMessage + | IChatTextEdit + | IChatConfirmation; export interface IChatFollowup { kind: 'reply'; @@ -173,15 +205,14 @@ export interface IChatFollowup { tooltip?: string; } -// Name has to match the one in vscode.d.ts for some reason -export enum InteractiveSessionVoteDirection { +export enum ChatAgentVoteDirection { Down = 0, Up = 1 } export interface IChatVoteAction { kind: 'vote'; - direction: InteractiveSessionVoteDirection; + direction: ChatAgentVoteDirection; reportIssue?: boolean; } @@ -227,12 +258,17 @@ export interface IChatBugReportAction { kind: 'bug'; } -export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction; +export interface IChatInlineChatCodeAction { + kind: 'inlineChat'; + action: 'accepted' | 'discarded'; +} + +export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction; export interface IChatUserActionEvent { action: ChatUserAction; - providerId: string; agentId: string | undefined; + command: string | undefined; sessionId: string; requestId: string; result: IChatAgentResult | undefined; @@ -270,44 +306,86 @@ export interface IChatTransferredSessionData { inputValue: string; } -export interface IChatSendRequestData { +export interface IChatSendRequestResponseState { + responseCreatedPromise: Promise; responseCompletePromise: Promise; +} + +export interface IChatSendRequestData extends IChatSendRequestResponseState { agent: IChatAgentData; slashCommand?: IChatAgentCommand; } +export interface IChatEditorLocationData { + type: ChatAgentLocation.Editor; + document: URI; + selection: ISelection; + wholeRange: IRange; +} + +export interface IChatNotebookLocationData { + type: ChatAgentLocation.Notebook; + sessionInputUri: URI; +} + +export interface IChatTerminalLocationData { + type: ChatAgentLocation.Terminal; + // TBD +} + +export type IChatLocationData = IChatEditorLocationData | IChatNotebookLocationData | IChatTerminalLocationData; + +export interface IChatSendRequestOptions { + location?: ChatAgentLocation; + locationData?: IChatLocationData; + parserContext?: IChatParserContext; + attempt?: number; + noCommandDetection?: boolean; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; + attachedContext?: IChatRequestVariableEntry[]; + + /** The target agent ID can be specified with this property instead of using @ in 'message' */ + agentId?: string; + slashCommand?: string; + + /** + * The label of the confirmation action that was selected. + */ + confirmation?: string; +} + export const IChatService = createDecorator('IChatService'); export interface IChatService { _serviceBrand: undefined; transferredSessionData: IChatTransferredSessionData | undefined; - onDidRegisterProvider: Event<{ providerId: string }>; - onDidUnregisterProvider: Event<{ providerId: string }>; - registerProvider(provider: IChatProvider): IDisposable; - hasSessions(providerId: string): boolean; - getProviderInfos(): IChatProviderInfo[]; - startSession(providerId: string, token: CancellationToken): ChatModel | undefined; + isEnabled(location: ChatAgentLocation): boolean; + hasSessions(): boolean; + startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel | undefined; getSession(sessionId: string): IChatModel | undefined; - getSessionId(sessionProviderId: number): string | undefined; getOrRestoreSession(sessionId: string): IChatModel | undefined; - loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined; + loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined; /** * Returns whether the request was accepted. */ - sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, location?: ChatAgentLocation, parserContext?: IChatParserContext): Promise; + sendRequest(sessionId: string, message: string, options?: IChatSendRequestOptions): Promise; + + resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; + adoptRequest(sessionId: string, request: IChatRequestModel): Promise; removeRequest(sessionid: string, requestId: string): Promise; cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; - addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void; + addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void; getHistory(): IChatDetail[]; clearAllHistoryEntries(): void; removeHistoryEntry(sessionId: string): void; onDidPerformUserAction: Event; notifyUserAction(event: IChatUserActionEvent): void; - onDidDisposeSession: Event<{ sessionId: string; providerId: string; reason: 'initializationFailed' | 'cleared' }>; + onDidDisposeSession: Event<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }>; transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index b932b682cbf..83741515df8 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -3,33 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, DisposableMap, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatRequestParser, IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_VOTE_UP_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; +import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatServiceTelemetry } from 'vs/workbench/contrib/chat/common/chatServiceTelemetry'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -44,100 +48,59 @@ interface IChatTransfer { const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60; type ChatProviderInvokedEvent = { - providerId: string; timeToFirstProgress: number | undefined; totalTime: number | undefined; result: 'success' | 'error' | 'errorWithOutput' | 'cancelled' | 'filtered'; requestType: 'string' | 'followup' | 'slashCommand'; chatSessionId: string; agent: string; + agentExtensionId: string | undefined; slashCommand: string | undefined; + location: ChatAgentLocation; + citations: number; }; type ChatProviderInvokedClassification = { - providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that was invoked.' }; - timeToFirstProgress: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The time in milliseconds from invoking the provider to getting the first data.' }; - totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The total time it took to run the provider\'s `provideResponseWithProgress`.' }; + timeToFirstProgress: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The time in milliseconds from invoking the provider to getting the first data.' }; + totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The total time it took to run the provider\'s `provideResponseWithProgress`.' }; result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the ChatProvider resulted in an error.' }; requestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of request that the user made.' }; chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A random ID for the session.' }; agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of agent used.' }; + agentExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the agent.' }; slashCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of slashCommand used.' }; + location: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location at which chat request was made.' }; + citations: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of public code citations that were returned with the response.' }; owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat providers.'; -}; - -type ChatVoteEvent = { - providerId: string; - direction: 'up' | 'down'; -}; - -type ChatVoteClassification = { - providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that this response came from.' }; - direction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user voted up or down.' }; - owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat providers.'; -}; - -type ChatCopyEvent = { - providerId: string; - copyKind: 'action' | 'toolbar'; -}; - -type ChatCopyClassification = { - providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that this codeblock response came from.' }; - copyKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the copy was initiated.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatInsertEvent = { - providerId: string; - newFile: boolean; -}; - -type ChatInsertClassification = { - providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that this codeblock response came from.' }; - newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatCommandEvent = { - providerId: string; - commandId: string; -}; - -type ChatCommandClassification = { - providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that this codeblock response came from.' }; - commandId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command that was executed.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatTerminalEvent = { - providerId: string; - languageId: string; -}; - -type ChatTerminalClassification = { - providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the provider that this codeblock response came from.' }; - languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language of the code that was run in the terminal.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; + comment: 'Provides insight into the performance of Chat agents.'; }; const maxPersistedSessions = 25; +class CancellableRequest implements IDisposable { + constructor( + public readonly cancellationTokenSource: CancellationTokenSource, + public requestId?: string | undefined + ) { } + + dispose() { + this.cancellationTokenSource.dispose(); + } + + cancel() { + this.cancellationTokenSource.cancel(); + } +} + export class ChatService extends Disposable implements IChatService { declare _serviceBrand: undefined; - private readonly _providers = new Map(); - private readonly _sessionModels = this._register(new DisposableMap()); - private readonly _pendingRequests = this._register(new DisposableMap()); + private readonly _pendingRequests = this._register(new DisposableMap()); private _persistedSessions: ISerializableChatsData; - private readonly _hasProvider: IContextKey; + + /** Just for empty windows, need to enforce that a chat was deleted, even though other windows still have it */ + private _deletedChatIds = new Set(); private _transferredSessionData: IChatTransferredSessionData | undefined; public get transferredSessionData(): IChatTransferredSessionData | undefined { @@ -147,16 +110,11 @@ export class ChatService extends Disposable implements IChatService { private readonly _onDidPerformUserAction = this._register(new Emitter()); public readonly onDidPerformUserAction: Event = this._onDidPerformUserAction.event; - private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; providerId: string; reason: 'initializationFailed' | 'cleared' }>()); + private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }>()); public readonly onDidDisposeSession = this._onDidDisposeSession.event; - private readonly _onDidRegisterProvider = this._register(new Emitter<{ providerId: string }>()); - public readonly onDidRegisterProvider = this._onDidRegisterProvider.event; - - private readonly _onDidUnregisterProvider = this._register(new Emitter<{ providerId: string }>()); - public readonly onDidUnregisterProvider = this._onDidUnregisterProvider.event; - private readonly _sessionFollowupCancelTokens = this._register(new DisposableMap()); + private readonly _chatServiceTelemetry: ChatServiceTelemetry; constructor( @IStorageService private readonly storageService: IStorageService, @@ -164,17 +122,19 @@ export class ChatService extends Disposable implements IChatService { @IExtensionService private readonly extensionService: IExtensionService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - @IChatAgentService private readonly chatAgentService: IChatAgentService + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IWorkbenchAssignmentService workbenchAssignmentService: IWorkbenchAssignmentService, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); - this._hasProvider = CONTEXT_PROVIDER_EXISTS.bindTo(this.contextKeyService); - - const sessionData = storageService.get(serializedChatKey, StorageScope.WORKSPACE, ''); + this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry); + const isEmptyWindow = !workspaceContextService.getWorkspace().folders.length; + const sessionData = storageService.get(serializedChatKey, isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, ''); if (sessionData) { this._persistedSessions = this.deserializeChats(sessionData); const countsForLog = Object.keys(this._persistedSessions).length; @@ -194,65 +154,109 @@ export class ChatService extends Disposable implements IChatService { } this._register(storageService.onWillSaveState(() => this.saveState())); + + const voteUpEnabled = CONTEXT_VOTE_UP_ENABLED.bindTo(contextKeyService); + workbenchAssignmentService.getTreatment('chatVoteUpEnabled') + .then(value => voteUpEnabled.set(!!value)); + } + + isEnabled(location: ChatAgentLocation): boolean { + return this.chatAgentService.getContributedDefaultAgent(location) !== undefined; } private saveState(): void { - let allSessions: (ChatModel | ISerializableChatData)[] = Array.from(this._sessionModels.values()) + const liveChats = Array.from(this._sessionModels.values()) + .filter(session => session.initialLocation === ChatAgentLocation.Panel) .filter(session => session.getRequests().length > 0); - allSessions = allSessions.concat( - Object.values(this._persistedSessions) - .filter(session => !this._sessionModels.has(session.sessionId)) - .filter(session => session.requests.length)); - allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); - allSessions = allSessions.slice(0, maxPersistedSessions); - if (allSessions.length) { - this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`); + + const isEmptyWindow = !this.workspaceContextService.getWorkspace().folders.length; + if (isEmptyWindow) { + this.syncEmptyWindowChats(liveChats); + } else { + let allSessions: (ChatModel | ISerializableChatData)[] = liveChats; + allSessions = allSessions.concat( + Object.values(this._persistedSessions) + .filter(session => !this._sessionModels.has(session.sessionId)) + .filter(session => session.requests.length)); + allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); + allSessions = allSessions.slice(0, maxPersistedSessions); + if (allSessions.length) { + this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`); + } + + const serialized = JSON.stringify(allSessions); + + if (allSessions.length) { + this.trace('onWillSaveState', `Persisting ${serialized.length} chars`); + } + + this.storageService.store(serializedChatKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE); } - const serialized = JSON.stringify(allSessions); + this._deletedChatIds.clear(); + } - if (allSessions.length) { - this.trace('onWillSaveState', `Persisting ${serialized.length} chars`); + private syncEmptyWindowChats(thisWindowChats: ChatModel[]): void { + // Note- an unavoidable race condition exists here. If there are multiple empty windows open, and the user quits the application, then the focused + // window may lose active chats, because all windows are reading and writing to storageService at the same time. This can't be fixed without some + // kind of locking, but in reality, the focused window will likely have run `saveState` at some point, like on a window focus change, and it will + // generally be fine. + const sessionData = this.storageService.get(serializedChatKey, StorageScope.APPLICATION, ''); + + const originalPersistedSessions = this._persistedSessions; + let persistedSessions: ISerializableChatsData; + if (sessionData) { + persistedSessions = this.deserializeChats(sessionData); + const countsForLog = Object.keys(persistedSessions).length; + if (countsForLog > 0) { + this.trace('constructor', `Restored ${countsForLog} persisted sessions`); + } + } else { + persistedSessions = {}; } - this.storageService.store(serializedChatKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this._deletedChatIds.forEach(id => delete persistedSessions[id]); + + // Has the chat in this window been updated, and then closed? Overwrite the old persisted chats. + Object.values(originalPersistedSessions).forEach(session => { + const persistedSession = persistedSessions[session.sessionId]; + if (persistedSession && session.requests.length > persistedSession.requests.length) { + // We will add a 'modified date' at some point, but comparing the number of requests is good enough + persistedSessions[session.sessionId] = session; + } else if (!persistedSession && session.isNew) { + // This session was created in this window, and hasn't been persisted yet + session.isNew = false; + persistedSessions[session.sessionId] = session; + } + }); + + this._persistedSessions = persistedSessions; + + // Add this window's active chat models to the set to persist. + // Having the same session open in two empty windows at the same time can lead to data loss, this is acceptable + const allSessions: Record = { ...this._persistedSessions }; + for (const chat of thisWindowChats) { + allSessions[chat.sessionId] = chat; + } + + let sessionsList = Object.values(allSessions); + sessionsList.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); + sessionsList = sessionsList.slice(0, maxPersistedSessions); + const data = JSON.stringify(sessionsList); + this.storageService.store(serializedChatKey, data, StorageScope.APPLICATION, StorageTarget.MACHINE); } notifyUserAction(action: IChatUserActionEvent): void { - if (action.action.kind === 'vote') { - this.telemetryService.publicLog2('interactiveSessionVote', { - providerId: action.providerId, - direction: action.action.direction === InteractiveSessionVoteDirection.Up ? 'up' : 'down' - }); - } else if (action.action.kind === 'copy') { - this.telemetryService.publicLog2('interactiveSessionCopy', { - providerId: action.providerId, - copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar' - }); - } else if (action.action.kind === 'insert') { - this.telemetryService.publicLog2('interactiveSessionInsert', { - providerId: action.providerId, - newFile: !!action.action.newFile - }); - } else if (action.action.kind === 'command') { - const command = CommandsRegistry.getCommand(action.action.commandButton.command.id); - const commandId = command ? action.action.commandButton.command.id : 'INVALID'; - this.telemetryService.publicLog2('interactiveSessionCommand', { - providerId: action.providerId, - commandId - }); - } else if (action.action.kind === 'runInTerminal') { - this.telemetryService.publicLog2('interactiveSessionRunInTerminal', { - providerId: action.providerId, - languageId: action.action.languageId ?? '' - }); - } - + this._chatServiceTelemetry.notifyUserAction(action); this._onDidPerformUserAction.fire(action); } - private trace(method: string, message: string): void { - this.logService.trace(`ChatService#${method}: ${message}`); + private trace(method: string, message?: string): void { + if (message) { + this.logService.trace(`ChatService#${method}: ${message}`); + } else { + this.logService.trace(`ChatService#${method}`); + } } private error(method: string, message: string): void { @@ -266,7 +270,7 @@ export class ChatService extends Disposable implements IChatService { throw new Error('Expected array'); } - const sessions = arrayOfSessions.reduce((acc, session) => { + const sessions = arrayOfSessions.reduce((acc, session) => { // Revive serialized markdown strings in response data for (const request of session.requests) { if (Array.isArray(request.response)) { @@ -283,7 +287,7 @@ export class ChatService extends Disposable implements IChatService { acc[session.sessionId] = session; return acc; - }, {} as ISerializableChatsData); + }, {}); return sessions; } catch (err) { this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`); @@ -332,75 +336,59 @@ export class ChatService extends Disposable implements IChatService { } removeHistoryEntry(sessionId: string): void { + this._deletedChatIds.add(sessionId); delete this._persistedSessions[sessionId]; this.saveState(); } clearAllHistoryEntries(): void { + Object.values(this._persistedSessions).forEach(session => this._deletedChatIds.add(session.sessionId)); this._persistedSessions = {}; this.saveState(); } - startSession(providerId: string, token: CancellationToken): ChatModel { - this.trace('startSession', `providerId=${providerId}`); - return this._startSession(providerId, undefined, token); + startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel { + this.trace('startSession'); + return this._startSession(undefined, location, token); } - private _startSession(providerId: string, someSessionHistory: ISerializableChatData | undefined, token: CancellationToken): ChatModel { - this.trace('_startSession', `providerId=${providerId}`); - const model = this.instantiationService.createInstance(ChatModel, providerId, someSessionHistory); + private _startSession(someSessionHistory: IExportableChatData | ISerializableChatData | undefined, location: ChatAgentLocation, token: CancellationToken): ChatModel { + const model = this.instantiationService.createInstance(ChatModel, someSessionHistory, location); this._sessionModels.set(model.sessionId, model); this.initializeSession(model, token); return model; } - private reinitializeModel(model: ChatModel): void { - this.trace('reinitializeModel', `Start reinit`); - this.initializeSession(model, CancellationToken.None); - } - private async initializeSession(model: ChatModel, token: CancellationToken): Promise { try { this.trace('initializeSession', `Initialize session ${model.sessionId}`); model.startInitialize(); - await this.extensionService.activateByEvent(`onInteractiveSession:${model.providerId}`); - const provider = this._providers.get(model.providerId); - if (!provider) { - throw new ErrorNoTelemetry(`Unknown provider: ${model.providerId}`); + await this.extensionService.whenInstalledExtensionsRegistered(); + const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(model.initialLocation) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel); + if (!defaultAgentData) { + throw new ErrorNoTelemetry('No default agent contributed'); } - let session: IChat | undefined; - try { - session = await provider.prepareSession(token) ?? undefined; - } catch (err) { - this.trace('initializeSession', `Provider initializeSession threw: ${err}`); - } + await this.extensionService.activateByEvent(`onChatParticipant:${defaultAgentData.id}`); - if (!session) { - throw new Error('Provider returned no session'); - } - - this.trace('startSession', `Provider returned session`); - - const defaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); + const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id); if (!defaultAgent) { - throw new ErrorNoTelemetry('No default agent'); + throw new ErrorNoTelemetry('No default agent registered'); } - - const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(token) ?? undefined; + const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(model.initialLocation, token) ?? undefined; const welcomeModel = welcomeMessage && this.instantiationService.createInstance( ChatWelcomeMessageModel, welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item), - await defaultAgent.provideSampleQuestions?.(token) ?? [] + await defaultAgent.provideSampleQuestions?.(model.initialLocation, token) ?? [] ); - model.initialize(session, welcomeModel); + model.initialize(welcomeModel); } catch (err) { this.trace('startSession', `initializeSession failed: ${err}`); model.setInitializationError(err); this._sessionModels.deleteAndDispose(model.sessionId); - this._onDidDisposeSession.fire({ sessionId: model.sessionId, providerId: model.providerId, reason: 'initializationFailed' }); + this._onDidDisposeSession.fire({ sessionId: model.sessionId, reason: 'initializationFailed' }); } } @@ -408,10 +396,6 @@ export class ChatService extends Disposable implements IChatService { return this._sessionModels.get(sessionId); } - getSessionId(sessionProviderId: number): string | undefined { - return Iterable.find(this._sessionModels.values(), model => model.session?.id === sessionProviderId)?.sessionId; - } - getOrRestoreSession(sessionId: string): ChatModel | undefined { this.trace('getOrRestoreSession', `sessionId: ${sessionId}`); const model = this._sessionModels.get(sessionId); @@ -419,7 +403,7 @@ export class ChatService extends Disposable implements IChatService { return model; } - const sessionData = this._persistedSessions[sessionId]; + const sessionData = revive(this._persistedSessions[sessionId]); if (!sessionData) { return undefined; } @@ -428,14 +412,39 @@ export class ChatService extends Disposable implements IChatService { this._transferredSessionData = undefined; } - return this._startSession(sessionData.providerId, sessionData, CancellationToken.None); + return this._startSession(sessionData, sessionData.initialLocation ?? ChatAgentLocation.Panel, CancellationToken.None); } - loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined { - return this._startSession(data.providerId, data, CancellationToken.None); + loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined { + return this._startSession(data, data.initialLocation ?? ChatAgentLocation.Panel, CancellationToken.None); } - async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean, location: ChatAgentLocation = ChatAgentLocation.Panel, parserContext?: IChatParserContext): Promise { + async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise { + const model = this._sessionModels.get(request.session.sessionId); + if (!model && model !== request.session) { + throw new Error(`Unknown session: ${request.session.sessionId}`); + } + + await model.waitForInitialization(); + + const cts = this._pendingRequests.get(request.session.sessionId); + if (cts) { + this.trace('resendRequest', `Session ${request.session.sessionId} already has a pending request, cancelling...`); + cts.cancel(); + } + + const location = options?.location ?? model.initialLocation; + const attempt = options?.attempt ?? 0; + const enableCommandDetection = !options?.noCommandDetection; + const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; + + model.removeRequest(request.id, ChatRequestRemovalReason.Resend); + + await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, options).responseCompletePromise; + } + + async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { + this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); if (!request.trim()) { this.trace('sendRequest', 'Rejected empty message'); @@ -448,30 +457,44 @@ export class ChatService extends Disposable implements IChatService { } await model.waitForInitialization(); - const provider = this._providers.get(model.providerId); - if (!provider) { - throw new Error(`Unknown provider: ${model.providerId}`); - } if (this._pendingRequests.has(sessionId)) { this.trace('sendRequest', `Session ${sessionId} already has a pending request`); return; } + const location = options?.location ?? model.initialLocation; + const attempt = options?.attempt ?? 0; const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); + const parsedRequest = this.parseChatRequest(sessionId, request, location, options); const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); // This method is only returning whether the request was accepted - don't block on the actual request return { - responseCompletePromise: this._sendRequestAsync(model, sessionId, provider, parsedRequest, implicitVariablesEnabled ?? false, defaultAgent, location), + ...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, defaultAgent, location, options), agent, slashCommand: agentSlashCommandPart?.command, }; } + private parseChatRequest(sessionId: string, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest { + let parserContext = options?.parserContext; + if (options?.agentId) { + const agent = this.chatAgentService.getAgent(options.agentId); + if (!agent) { + throw new Error(`Unknown agent: ${options.agentId}`); + } + parserContext = { selectedAgent: agent }; + const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : ''; + request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`; + } + + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); + return parsedRequest; + } + private refreshFollowupsCancellationToken(sessionId: string): CancellationToken { this._sessionFollowupCancelTokens.get(sessionId)?.cancel(); const newTokenSource = new CancellationTokenSource(); @@ -480,7 +503,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, parsedRequest: IParsedChatRequest, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation): Promise { + private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -490,6 +513,15 @@ export class ChatService extends Disposable implements IChatService { let gotProgress = false; const requestType = commandPart ? 'slashCommand' : 'string'; + const responseCreated = new DeferredPromise(); + let responseCreatedComplete = false; + function completeResponseCreated(): void { + if (!responseCreatedComplete && request?.response) { + responseCreated.complete(request.response); + responseCreatedComplete = true; + } + } + const source = new CancellationTokenSource(); const token = source.token; const sendRequestInternal = async () => { @@ -500,28 +532,31 @@ export class ChatService extends Disposable implements IChatService { gotProgress = true; - if (progress.kind === 'content' || progress.kind === 'markdownContent') { - this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${typeof progress.content === 'string' ? progress.content.length : progress.content.value.length} chars`); + if (progress.kind === 'markdownContent') { + this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.value.length} chars`); } else { this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progress)}`); } model.acceptResponseProgress(request, progress); + completeResponseCreated(); }; const stopWatch = new StopWatch(false); const listener = token.onCancellationRequested(() => { this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`); this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { - providerId: provider.id, timeToFirstProgress: undefined, // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling totalTime: stopWatch.elapsed(), result: 'cancelled', requestType, agent: agentPart?.agent.id ?? '', + agentExtensionId: agentPart?.agent.extensionId.value ?? '', slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId + chatSessionId: model.sessionId, + location, + citations: request?.response?.codeCitations.length ?? 0 }); model.cancelRequest(request); @@ -531,41 +566,70 @@ export class ChatService extends Disposable implements IChatService { let rawResult: IChatAgentResult | null | undefined; let agentOrCommandFollowups: Promise | undefined = undefined; + if (agentPart || (defaultAgent && !commandPart)) { - const agent = (agentPart?.agent ?? defaultAgent)!; - await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); - const history = getHistoryEntriesFromModel(model); + const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, chatRequest?: ChatRequestModel) => { + const initVariableData: IChatRequestVariableData = { variables: [] }; + request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation); - const initVariableData: IChatRequestVariableData = { variables: [] }; - request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command); - const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, progressCallback, token); - request.variableData = variableData; + // Variables may have changed if the agent and slash command changed, so resolve them again even if we already had a chatRequest + const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token); + model.updateRequest(request, variableData); + const promptTextResult = getPromptText(request.message); + const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack - const promptTextResult = getPromptText(request.message); - const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack - if (implicitVariablesEnabled) { - const implicitVariables = agent.defaultImplicitVariables; - if (implicitVariables) { - const resolvedImplicitVariables = await Promise.all(implicitVariables.map(async v => ({ name: v, values: await this.chatVariablesService.resolveVariable(v, parsedRequest.text, model, progressCallback, token) } satisfies IChatRequestVariableEntry))); - updatedVariableData.variables.push(...resolvedImplicitVariables); + return { + sessionId, + requestId: request.id, + agentId: agent.id, + message: promptTextResult.message, + command: command?.name, + variables: updatedVariableData, + enableCommandDetection, + attempt, + location, + locationData: options?.locationData, + acceptedConfirmationData: options?.acceptedConfirmationData, + rejectedConfirmationData: options?.rejectedConfirmationData, + } satisfies IChatAgentRequest; + }; + + let detectedAgent: IChatAgentData | undefined; + let detectedCommand: IChatAgentCommand | undefined; + if (this.configurationService.getValue('chat.experimental.detectParticipant.enabled') && !agentPart && !commandPart && enableCommandDetection) { + // We have no agent or command to scope history with, pass the full history to the participant detection provider + const defaultAgentHistory = getHistoryEntriesFromModel(model, defaultAgent.id); + + // Prepare the request object that we will send to the participant detection provider + const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, agentSlashCommandPart?.command); + + const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token); + if (result) { + // Update the response in the ChatModel to reflect the detected agent and command + request.response?.setAgent(result.agent, result.command); + detectedAgent = result.agent; + detectedCommand = result.command; } } - const requestProps: IChatAgentRequest = { - sessionId, - requestId: request.id, - agentId: agent.id, - message: promptTextResult.message, - command: agentSlashCommandPart?.command.name, - variables: updatedVariableData, - location - }; + const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!; + const command = detectedCommand ?? agentSlashCommandPart?.command; + await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); + // Recompute history in case the agent or command changed + const history = getHistoryEntriesFromModel(model, agent.id); + const requestProps = await prepareChatAgentRequest(agent, command, request /* Reuse the request object if we already created it for participant detection */); + const pendingRequest = this._pendingRequests.get(sessionId); + if (pendingRequest && !pendingRequest.requestId) { + pendingRequest.requestId = requestProps.requestId; + } + completeResponseCreated(); const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); rawResult = agentResult; agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { - request = model.addRequest(parsedRequest, { variables: [] }); + request = model.addRequest(parsedRequest, { variables: [] }, attempt); + completeResponseCreated(); // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; @@ -573,13 +637,13 @@ export class ChatService extends Disposable implements IChatService { if (!request.response) { continue; } - history.push({ role: ChatMessageRole.User, content: request.message.text }); - history.push({ role: ChatMessageRole.Assistant, content: request.response.response.asString() }); + history.push({ role: ChatMessageRole.User, content: { type: 'text', value: request.message.text } }); + history.push({ role: ChatMessageRole.Assistant, content: { type: 'text', value: request.response.response.toString() } }); } const message = parsedRequest.text; const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { progressCallback(p); - }), history, token); + }), history, location, token); agentOrCommandFollowups = Promise.resolve(commandResult?.followUp); rawResult = {}; @@ -599,36 +663,65 @@ export class ChatService extends Disposable implements IChatService { rawResult.errorDetails && gotProgress ? 'errorWithOutput' : rawResult.errorDetails ? 'error' : 'success'; + const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command; this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { - providerId: provider.id, timeToFirstProgress: rawResult.timings?.firstProgress, totalTime: rawResult.timings?.totalElapsed, result, requestType, agent: agentPart?.agent.id ?? '', - slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId + agentExtensionId: agentPart?.agent.extensionId.value ?? '', + slashCommand: commandForTelemetry, + chatSessionId: model.sessionId, + location, + citations: request.response?.codeCitations.length ?? 0 }); model.setResponse(request, rawResult); + completeResponseCreated(); this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); model.completeResponse(request); if (agentOrCommandFollowups) { agentOrCommandFollowups.then(followups => { model.setFollowups(request, followups); + this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0); }); } } + } catch (err) { + const result = 'error'; + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + timeToFirstProgress: undefined, + totalTime: undefined, + result, + requestType, + agent: agentPart?.agent.id ?? '', + agentExtensionId: agentPart?.agent.extensionId.value ?? '', + slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, + chatSessionId: model.sessionId, + location, + citations: 0 + }); + this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`); + if (request) { + const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; + model.setResponse(request, rawResult); + completeResponseCreated(); + model.completeResponse(request); + } } finally { listener.dispose(); } }; const rawResponsePromise = sendRequestInternal(); - this._pendingRequests.set(model.sessionId, source); + this._pendingRequests.set(model.sessionId, new CancellableRequest(source)); rawResponsePromise.finally(() => { this._pendingRequests.deleteAndDispose(model.sessionId); }); - return rawResponsePromise; + return { + responseCreatedPromise: responseCreated.p, + responseCompletePromise: rawResponsePromise, + }; } async removeRequest(sessionId: string, requestId: string): Promise { @@ -638,19 +731,40 @@ export class ChatService extends Disposable implements IChatService { } await model.waitForInitialization(); - const provider = this._providers.get(model.providerId); - if (!provider) { - throw new Error(`Unknown provider: ${model.providerId}`); + + const pendingRequest = this._pendingRequests.get(sessionId); + if (pendingRequest?.requestId === requestId) { + pendingRequest.cancel(); + this._pendingRequests.deleteAndDispose(sessionId); } model.removeRequest(requestId); } - getProviders(): string[] { - return Array.from(this._providers.keys()); + async adoptRequest(sessionId: string, request: IChatRequestModel) { + if (!(request instanceof ChatRequestModel)) { + throw new TypeError('Can only adopt requests of type ChatRequestModel'); + } + const target = this._sessionModels.get(sessionId); + if (!target) { + throw new Error(`Unknown session: ${sessionId}`); + } + + await target.waitForInitialization(); + + const oldOwner = request.session; + target.adoptRequest(request); + + if (request.response && !request.response.isComplete) { + const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionId); + if (cts) { + cts.requestId = request.id; + this._pendingRequests.set(target.sessionId, cts); + } + } } - async addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): Promise { + async addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise { this.trace('addCompleteRequest', `message: ${message}`); const model = this._sessionModels.get(sessionId); @@ -662,9 +776,10 @@ export class ChatService extends Disposable implements IChatService { const parsedRequest = typeof message === 'string' ? this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) : message; - const request = model.addRequest(parsedRequest, variableData || { variables: [] }); + const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0); if (typeof response.message === 'string') { - model.acceptResponseProgress(request, { content: response.message, kind: 'content' }); + // TODO is this possible? + model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' }); } else { for (const part of response.message) { model.acceptResponseProgress(request, part, true); @@ -690,52 +805,22 @@ export class ChatService extends Disposable implements IChatService { throw new Error(`Unknown session: ${sessionId}`); } - this._persistedSessions[sessionId] = model.toJSON(); + if (model.initialLocation === ChatAgentLocation.Panel) { + // Turn all the real objects into actual JSON, otherwise, calling 'revive' may fail when it tries to + // assign values to properties that are getters- microsoft/vscode-copilot-release#1233 + const sessionData: ISerializableChatData = JSON.parse(JSON.stringify(model)); + sessionData.isNew = true; + this._persistedSessions[sessionId] = sessionData; + } this._sessionModels.deleteAndDispose(sessionId); this._pendingRequests.get(sessionId)?.cancel(); this._pendingRequests.deleteAndDispose(sessionId); - this._onDidDisposeSession.fire({ sessionId, providerId: model.providerId, reason: 'cleared' }); + this._onDidDisposeSession.fire({ sessionId, reason: 'cleared' }); } - registerProvider(provider: IChatProvider): IDisposable { - this.trace('registerProvider', `Adding new chat provider`); - - if (this._providers.has(provider.id)) { - throw new Error(`Provider ${provider.id} already registered`); - } - - this._providers.set(provider.id, provider); - this._hasProvider.set(true); - this._onDidRegisterProvider.fire({ providerId: provider.id }); - - Array.from(this._sessionModels.values()) - .filter(model => model.providerId === provider.id) - // The provider may have been registered in the process of initializing this model. Only grab models that were deinitialized when the provider was unregistered - .filter(model => model.initState === ChatModelInitState.Created) - .forEach(model => this.reinitializeModel(model)); - - return toDisposable(() => { - this.trace('registerProvider', `Disposing chat provider`); - this._providers.delete(provider.id); - this._hasProvider.set(this._providers.size > 0); - Array.from(this._sessionModels.values()) - .filter(model => model.providerId === provider.id) - .forEach(model => model.deinitialize()); - this._onDidUnregisterProvider.fire({ providerId: provider.id }); - }); - } - - public hasSessions(providerId: string): boolean { - return !!Object.values(this._persistedSessions).find((session) => session.providerId === providerId); - } - - getProviderInfos(): IChatProviderInfo[] { - return Array.from(this._providers.values()).map(provider => { - return { - id: provider.id, - }; - }); + public hasSessions(): boolean { + return !!Object.values(this._persistedSessions); } transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { diff --git a/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts new file mode 100644 index 00000000000..ff4adf69d86 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IChatUserActionEvent, ChatAgentVoteDirection, ChatCopyKind } from 'vs/workbench/contrib/chat/common/chatService'; + +type ChatVoteEvent = { + direction: 'up' | 'down'; + agentId: string; + command: string | undefined; +}; + +type ChatVoteClassification = { + direction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user voted up or down.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this vote is for.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command that this vote is for.' }; + owner: 'roblourens'; + comment: 'Provides insight into the performance of Chat agents.'; +}; + +type ChatCopyEvent = { + copyKind: 'action' | 'toolbar'; + agentId: string; + command: string | undefined; +}; + +type ChatCopyClassification = { + copyKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the copy was initiated.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that the copy acted on.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command the copy acted on.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatInsertEvent = { + newFile: boolean; + agentId: string; + command: string | undefined; +}; + +type ChatInsertClassification = { + newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this insertion is for.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command that this insertion is for.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatCommandEvent = { + commandId: string; + agentId: string; + command: string | undefined; +}; + +type ChatCommandClassification = { + commandId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command that was executed.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatFollowupEvent = { + agentId: string; + command: string | undefined; +}; + +type ChatFollowupClassification = { + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatTerminalEvent = { + languageId: string; + agentId: string; + command: string | undefined; +}; + +type ChatTerminalClassification = { + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language of the code that was run in the terminal.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatFollowupsRetrievedEvent = { + agentId: string; + command: string | undefined; + numFollowups: number; +}; + +type ChatFollowupsRetrievedClassification = { + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + numFollowups: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of followup prompts returned by the agent.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +export class ChatServiceTelemetry { + constructor( + @ITelemetryService private readonly telemetryService: ITelemetryService, + ) { } + + notifyUserAction(action: IChatUserActionEvent): void { + if (action.action.kind === 'vote') { + this.telemetryService.publicLog2('interactiveSessionVote', { + direction: action.action.direction === ChatAgentVoteDirection.Up ? 'up' : 'down', + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'copy') { + this.telemetryService.publicLog2('interactiveSessionCopy', { + copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar', + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'insert') { + this.telemetryService.publicLog2('interactiveSessionInsert', { + newFile: !!action.action.newFile, + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'command') { + // TODO not currently called + const command = CommandsRegistry.getCommand(action.action.commandButton.command.id); + const commandId = command ? action.action.commandButton.command.id : 'INVALID'; + this.telemetryService.publicLog2('interactiveSessionCommand', { + commandId, + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'runInTerminal') { + this.telemetryService.publicLog2('interactiveSessionRunInTerminal', { + languageId: action.action.languageId ?? '', + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'followUp') { + this.telemetryService.publicLog2('chatFollowupClicked', { + agentId: action.agentId ?? '', + command: action.command, + }); + } + } + + retrievedFollowups(agentId: string, command: string | undefined, numFollowups: number): void { + this.telemetryService.publicLog2('chatFollowupsRetrieved', { + agentId, + command, + numFollowups, + }); + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts index 2d43f1c0396..3b19cd512b9 100644 --- a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts @@ -11,6 +11,7 @@ import { IProgress } from 'vs/platform/progress/common/progress'; import { IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; //#region slash service, commands etc @@ -18,18 +19,18 @@ export interface IChatSlashData { command: string; detail: string; sortText?: string; - /** * Whether the command should execute as soon * as it is entered. Defaults to `false`. */ executeImmediately?: boolean; + locations: ChatAgentLocation[]; } export interface IChatSlashFragment { content: string | { treeData: IChatResponseProgressFileTreeData }; } -export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; +export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; export const IChatSlashCommandService = createDecorator('chatSlashCommandService'); @@ -40,8 +41,8 @@ export interface IChatSlashCommandService { _serviceBrand: undefined; readonly onDidChangeCommands: Event; registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable; - executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; - getCommands(): Array; + executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; + getCommands(location: ChatAgentLocation): Array; hasCommand(id: string): boolean; } @@ -80,15 +81,15 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom }); } - getCommands(): Array { - return Array.from(this._commands.values(), v => v.data); + getCommands(location: ChatAgentLocation): Array { + return Array.from(this._commands.values(), v => v.data).filter(c => c.locations.includes(location)); } hasCommand(id: string): boolean { return this._commands.has(id); } - async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { + async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { const data = this._commands.get(id); if (!data) { throw new Error('No command with id ${id} NOT registered'); @@ -100,6 +101,6 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom throw new Error(`No command with id ${id} NOT resolved`); } - return await data.command(prompt, progress, history, token); + return await data.command(prompt, progress, history, location, token); } } diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index dc999f8081f..8f8e394ae65 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -5,34 +5,35 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; +import { Location } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference, IChatProgressMessage } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatVariableData { + id: string; name: string; + icon?: ThemeIcon; + fullName?: string; description: string; - hidden?: boolean; + modelDescription?: string; + isSlow?: boolean; canTakeArgument?: boolean; } -export interface IChatRequestVariableValue { - level: 'short' | 'medium' | 'full'; - kind?: string; - value: string | URI; - description?: string; -} +export type IChatRequestVariableValue = string | URI | Location | unknown; export type IChatVariableResolverProgress = | IChatContentReference | IChatProgressMessage; export interface IChatVariableResolver { - // TODO should we spec "zoom level" - (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; + (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; } export const IChatVariablesService = createDecorator('IChatVariablesService'); @@ -42,17 +43,23 @@ export interface IChatVariablesService { registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable; hasVariable(name: string): boolean; getVariable(name: string): IChatVariableData | undefined; - getVariables(): Iterable>; + getVariables(location: ChatAgentLocation): Iterable>; getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? + attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void; /** * Resolves all variables that occur in `prompt` */ - resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; - resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; + resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; + resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; } export interface IDynamicVariable { range: IRange; - data: IChatRequestVariableValue[]; + id: string; + fullName?: string; + icon?: ThemeIcon; + prefix?: string; + modelDescription?: string; + data: IChatRequestVariableValue; } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 90ec8c728fd..44c6ea66e9e 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -4,21 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; import { marked } from 'vs/base/common/marked/marked'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { TextEdit } from 'vs/editor/common/languages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; -import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; +import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; +import { hash } from 'vs/base/common/hash'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { return !!item && typeof item === 'object' && 'message' in item; @@ -49,7 +49,6 @@ export interface IChatSessionInitEvent { export interface IChatViewModel { readonly model: IChatModel; readonly initState: ChatModelInitState; - readonly providerId: string; readonly sessionId: string; readonly onDidDisposeModel: Event; readonly onDidChange: Event; @@ -69,13 +68,25 @@ export interface IChatRequestViewModel { readonly avatarIcon?: URI | ThemeIcon; readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; + readonly attempt: number; + readonly variables: IChatRequestVariableEntry[]; currentRenderedHeight: number | undefined; + readonly contentReferences?: ReadonlyArray; + readonly confirmation?: string; } export interface IChatResponseMarkdownRenderData { renderedWordCount: number; lastRenderTime: number; isFullyRendered: boolean; + originalMarkdown: IMarkdownString; +} + +export interface IChatResponseMarkdownRenderData2 { + renderedWordCount: number; + lastRenderTime: number; + isFullyRendered: boolean; + originalMarkdown: IMarkdownString; } export interface IChatProgressMessageRenderData { @@ -95,24 +106,53 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton; -export interface IChatResponseRenderData { - renderedParts: IChatRenderData[]; +export interface IChatTaskRenderData { + task: IChatTask; + isSettled: boolean; + progressLength: number; } +export interface IChatResponseRenderData { + renderedParts: IChatRendererContent[]; + + renderedWordCount: number; + lastRenderTime: number; +} + +/** + * Content type for references used during rendering, not in the model + */ +export interface IChatReferences { + references: ReadonlyArray; + kind: 'references'; +} + +/** + * Content type for citations used during rendering, not in the model + */ +export interface IChatCodeCitations { + citations: ReadonlyArray; + kind: 'codeCitations'; +} + +/** + * Type for content parts rendered by IChatListRenderer + */ +export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations; + export interface IChatLiveUpdateData { - loadingStartTime: number; + firstWordTime: number; lastUpdateTime: number; impliedWordLoadRate: number; lastWordCount: number; } export interface IChatResponseViewModel { + readonly model: IChatResponseModel; readonly id: string; readonly sessionId: string; /** This ID updates every time the underlying data changes */ readonly dataId: string; - readonly providerId: string; /** The ID of the associated IChatRequestViewModel */ readonly requestId: string; readonly username: string; @@ -123,22 +163,22 @@ export interface IChatResponseViewModel { readonly response: IResponse; readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; + readonly codeCitations: ReadonlyArray; readonly progressMessages: ReadonlyArray; - readonly edits: ResourceMap; readonly isComplete: boolean; readonly isCanceled: boolean; readonly isStale: boolean; - readonly vote: InteractiveSessionVoteDirection | undefined; + readonly vote: ChatAgentVoteDirection | undefined; readonly replyFollowups?: IChatFollowup[]; readonly errorDetails?: IChatResponseErrorDetails; readonly result?: IChatAgentResult; readonly contentUpdateTimings?: IChatLiveUpdateData; renderData?: IChatResponseRenderData; - agentAvatarHasBeenRendered?: boolean; currentRenderedHeight: number | undefined; - setVote(vote: InteractiveSessionVoteDirection): void; + setVote(vote: ChatAgentVoteDirection): void; usedReferencesExpanded?: boolean; vulnerabilitiesListExpanded: boolean; + setEditApplied(edit: IChatTextEditGroup, editCount: number): void; } export class ChatViewModel extends Disposable implements IChatViewModel { @@ -178,10 +218,6 @@ export class ChatViewModel extends Disposable implements IChatViewModel { return this._model.requestInProgress; } - get providerId() { - return this._model.providerId; - } - get initState() { return this._model.initState; } @@ -241,7 +277,9 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private onAddResponse(responseModel: IChatResponseModel) { const response = this.instantiationService.createInstance(ChatResponseViewModel, responseModel); this._register(response.onDidChange(() => { - this.updateCodeBlockTextModels(response); + if (response.isComplete) { + this.updateCodeBlockTextModels(response); + } return this._onDidChange.fire(null); })); this._items.push(response); @@ -271,24 +309,13 @@ export class ChatViewModel extends Disposable implements IChatViewModel { const renderer = new marked.Renderer(); renderer.code = (value, languageId) => { languageId ??= ''; - const newText = this.fixCodeText(value, languageId); - this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: newText, languageId }); + this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: value, languageId }); return ''; }; marked.parse(this.ensureFencedCodeBlocksTerminated(content), { renderer }); } - private fixCodeText(text: string, languageId: string): string { - if (languageId === 'php') { - if (!text.trim().startsWith('<')) { - return ``; - } - } - - return text; - } - /** * Marked doesn't consistently render fenced code blocks that aren't terminated. * @@ -296,18 +323,26 @@ export class ChatViewModel extends Disposable implements IChatViewModel { */ private ensureFencedCodeBlocksTerminated(content: string): string { const lines = content.split('\n'); - let inCodeBlock = false; + let codeBlockState: undefined | { readonly delimiter: string; readonly indent: string }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; - if (line.startsWith('```')) { - inCodeBlock = !inCodeBlock; + + if (codeBlockState) { + if (new RegExp(`^\\s*${codeBlockState.delimiter}\\s*$`).test(line)) { + codeBlockState = undefined; + } + } else { + const match = line.match(/^(\s*)(`{3,}|~{3,}|)/); + if (match) { + codeBlockState = { delimiter: match[2], indent: match[1] }; + } } } // If we're still in a code block at the end of the content, add a closing fence - if (inCodeBlock) { - lines.push('```'); + if (codeBlockState) { + lines.push(codeBlockState.indent + codeBlockState.delimiter); } return lines.join('\n'); @@ -320,7 +355,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { } get dataId() { - return this.id + `_${ChatModelInitState[this._model.session.initState]}`; + return this.id + `_${ChatModelInitState[this._model.session.initState]}_${hash(this.variables)}`; } get sessionId() { @@ -343,10 +378,26 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this.message.text; } + get attempt() { + return this._model.attempt; + } + + get variables() { + return this._model.variableData.variables; + } + + get contentReferences() { + return this._model.response?.contentReferences; + } + + get confirmation() { + return this._model.confirmation; + } + currentRenderedHeight: number | undefined; constructor( - readonly _model: IChatRequestModel, + private readonly _model: IChatRequestModel, ) { } } @@ -356,6 +407,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; + get model() { + return this._model; + } + get id() { return this._model.id; } @@ -364,15 +419,20 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.id + `_${this._modelChangeCount}` + `_${ChatModelInitState[this._model.session.initState]}`; } - get providerId() { - return this._model.providerId; - } - get sessionId() { return this._model.session.sessionId; } get username() { + if (this.agent) { + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(this.agent); + if (isAllowed) { + return this.agent.fullName || this.agent.name; + } else { + return getFullyQualifiedId(this.agent); + } + } + return this._model.username; } @@ -404,12 +464,12 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.contentReferences; } - get progressMessages(): ReadonlyArray { - return this._model.progressMessages; + get codeCitations(): ReadonlyArray { + return this._model.codeCitations; } - get edits(): ResourceMap { - return this._model.edits; + get progressMessages(): ReadonlyArray { + return this._model.progressMessages; } get isComplete() { @@ -445,7 +505,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } renderData: IChatResponseRenderData | undefined = undefined; - agentAvatarHasBeenRendered?: boolean; currentRenderedHeight: number | undefined; private _usedReferencesExpanded: boolean | undefined; @@ -478,12 +537,13 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi constructor( private readonly _model: IChatResponseModel, @ILogService private readonly logService: ILogService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); if (!_model.isComplete) { this._contentUpdateTimings = { - loadingStartTime: Date.now(), + firstWordTime: 0, lastUpdateTime: Date.now(), impliedWordLoadRate: 0, lastWordCount: 0 @@ -491,15 +551,17 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } this._register(_model.onDidChange(() => { + // This should be true, if the model is changing if (this._contentUpdateTimings) { - // This should be true, if the model is changing const now = Date.now(); - const wordCount = countWords(_model.response.asString()); - const timeDiff = now - this._contentUpdateTimings.loadingStartTime; + const wordCount = countWords(_model.response.toString()); + + // Apply a min time difference, or the rate is typically too high for first few words + const timeDiff = Math.max(now - this._contentUpdateTimings.firstWordTime, 250); const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (timeDiff / 1000); - this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); + this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); this._contentUpdateTimings = { - loadingStartTime: this._contentUpdateTimings.loadingStartTime, + firstWordTime: this._contentUpdateTimings.firstWordTime === 0 && this.response.value.some(v => v.kind === 'markdownContent') ? now : this._contentUpdateTimings.firstWordTime, lastUpdateTime: now, impliedWordLoadRate, lastWordCount: wordCount @@ -519,10 +581,15 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi this.logService.trace(`ChatResponseViewModel#${tag}: ${message}`); } - setVote(vote: InteractiveSessionVoteDirection): void { + setVote(vote: ChatAgentVoteDirection): void { this._modelChangeCount++; this._model.setVote(vote); } + + setEditApplied(edit: IChatTextEditGroup, editCount: number) { + this._modelChangeCount++; + this._model.setEditApplied(edit, editCount); + } } export interface IChatWelcomeMessageViewModel { diff --git a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index 03948260a67..01716421e7b 100644 --- a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts @@ -7,6 +7,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; export interface IChatHistoryEntry { text: string; @@ -20,8 +22,8 @@ export interface IChatWidgetHistoryService { readonly onDidClearHistory: Event; clearHistory(): void; - getHistory(providerId: string): IChatHistoryEntry[]; - saveHistory(providerId: string, history: IChatHistoryEntry[]): void; + getHistory(location: ChatAgentLocation): IChatHistoryEntry[]; + saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void; } interface IChatHistory { @@ -50,15 +52,23 @@ export class ChatWidgetHistoryService implements IChatWidgetHistoryService { this.viewState = loadedState; } - getHistory(providerId: string): IChatHistoryEntry[] { - return this.viewState.history?.[providerId] ?? []; + getHistory(location: ChatAgentLocation): IChatHistoryEntry[] { + const key = this.getKey(location); + return this.viewState.history?.[key] ?? []; } - saveHistory(providerId: string, history: IChatHistoryEntry[]): void { + private getKey(location: ChatAgentLocation): string { + // Preserve history for panel by continuing to use the same old provider id. Use the location as a key for other chat locations. + return location === ChatAgentLocation.Panel ? CHAT_PROVIDER_ID : location; + } + + saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void { if (!this.viewState.history) { this.viewState.history = {}; } - this.viewState.history[providerId] = history; + + const key = this.getKey(location); + this.viewState.history[key] = history; this.memento.saveMemento(); } diff --git a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts index eeb2d6691fb..c1989d4f997 100644 --- a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts +++ b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts @@ -3,40 +3,67 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const wordSeparatorCharPattern = /[\s\|\-]/; - export interface IWordCountResult { value: string; - actualWordCount: number; + returnedWordCount: number; + totalWordCount: number; isFullString: boolean; } +const r = String.raw; + +/** + * Matches `[text](link title?)` or `[text]( title?)` + * + * Taken from vscode-markdown-languageservice + */ +const linkPattern = + r`(? + /**/r`(?:` + + /*****/r`[^\[\]\\]|` + // Non-bracket chars, or... + /*****/r`\\.|` + // Escaped char, or... + /*****/r`\[[^\[\]]*\]` + // Matched bracket pair + /**/r`)*` + + r`\])` + // <-- close prefix match + + // Destination + r`(\(\s*)` + // Pre href + /**/r`(` + + /*****/r`[^\s\(\)<](?:[^\s\(\)]|\([^\s\(\)]*?\))*|` + // Link without whitespace, or... + /*****/r`<(?:\\[<>]|[^<>])+>` + // In angle brackets + /**/r`)` + + + // Title + /**/r`\s*(?:"[^"]*"|'[^']*'|\([^\(\)]*\))?\s*` + + r`\)`; + export function getNWords(str: string, numWordsToCount: number): IWordCountResult { - let wordCount = numWordsToCount; - let i = 0; - while (i < str.length && wordCount > 0) { - // Consume word separator chars - while (i < str.length && str[i].match(wordSeparatorCharPattern)) { - i++; - } + // This regex matches each word and skips over whitespace and separators. A word is: + // A markdown link + // One chinese character + // One or more + - =, handled so that code like "a=1+2-3" is broken up better + // One or more characters that aren't whitepace or any of the above + const allWordMatches = Array.from(str.matchAll(new RegExp(linkPattern + r`|\p{sc=Han}|=+|\++|-+|[^\s\|\p{sc=Han}|=|\+|\-]+`, 'gu'))); - // Consume word chars - while (i < str.length && !str[i].match(wordSeparatorCharPattern)) { - i++; - } + const targetWords = allWordMatches.slice(0, numWordsToCount); - wordCount--; - } + const endIndex = numWordsToCount > allWordMatches.length + ? str.length // Reached end of string + : targetWords.length ? targetWords.at(-1)!.index + targetWords.at(-1)![0].length : 0; - const value = str.substring(0, i); + const value = str.substring(0, endIndex); return { value, - actualWordCount: numWordsToCount - wordCount, - isFullString: i >= str.length + returnedWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, + isFullString: endIndex >= str.length, + totalWordCount: allWordMatches.length }; } export function countWords(str: string): number { const result = getNWords(str, Number.MAX_SAFE_INTEGER); - return result.actualWordCount; + return result.returnedWordCount; } diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index f364b9e78ab..d5ed699cd9b 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -22,6 +22,13 @@ export class CodeBlockModelCollection extends Disposable { vulns: readonly IMarkdownVulnerability[]; }>(); + /** + * Max number of models to keep in memory. + * + * Currently always maintains the most recently created models. + */ + private readonly maxModelCount = 100; + constructor( @ILanguageService private readonly languageService: ILanguageService, @ITextModelService private readonly textModelService: ITextModelService @@ -52,9 +59,28 @@ export class CodeBlockModelCollection extends Disposable { const uri = this.getUri(sessionId, chat, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); this._models.set(uri, { model: ref, vulns: [] }); + + while (this._models.size > this.maxModelCount) { + const first = Array.from(this._models.keys()).at(0); + if (!first) { + break; + } + this.delete(first); + } + return { model: ref.then(ref => ref.object), vulns: [] }; } + private delete(codeBlockUri: URI) { + const entry = this._models.get(codeBlockUri); + if (!entry) { + return; + } + + entry.model.then(ref => ref.dispose()); + this._models.delete(codeBlockUri); + } + clear(): void { this._models.forEach(async entry => (await entry.model).dispose()); this._models.clear(); @@ -64,7 +90,7 @@ export class CodeBlockModelCollection extends Disposable { const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); const extractedVulns = extractVulnerabilitiesFromText(content.text); - const newText = extractedVulns.newText; + const newText = fixCodeText(extractedVulns.newText, content.languageId); this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); const textModel = (await entry.model).textEditorModel; @@ -137,3 +163,13 @@ export class CodeBlockModelCollection extends Disposable { }; } } + +function fixCodeText(text: string, languageId: string | undefined): string { + if (languageId === 'php') { + if (!text.trim().startsWith('<')) { + return `('ILanguageModelStatsService'); + +export interface ILanguageModelStatsService { + readonly _serviceBrand: undefined; + + update(model: string, extensionId: ExtensionIdentifier, agent: string | undefined, tokenCount: number | undefined): Promise; +} + +interface LanguageModelStats { + extensions: { + extensionId: string; + requestCount: number; + tokenCount: number; + participants: { + id: string; + requestCount: number; + tokenCount: number; + }[]; + }[]; +} + +export class LanguageModelStatsService extends Disposable implements ILanguageModelStatsService { + + private static readonly MODEL_STATS_STORAGE_KEY_PREFIX = 'languageModelStats.'; + private static readonly MODEL_ACCESS_STORAGE_KEY_PREFIX = 'languageModelAccess.'; + + declare _serviceBrand: undefined; + + private readonly _onDidChangeStats = this._register(new Emitter()); + readonly onDidChangeLanguageMoelStats = this._onDidChangeStats.event; + + private readonly sessionStats = new Map(); + + constructor( + @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, + @IStorageService private readonly _storageService: IStorageService, + ) { + super(); + this._register(_storageService.onDidChangeValue(StorageScope.APPLICATION, undefined, this._store)(e => { + const model = this.getModel(e.key); + if (model) { + this._onDidChangeStats.fire(model); + } + })); + } + + hasAccessedModel(extensionId: string, model: string): boolean { + return this.getAccessExtensions(model).includes(extensionId.toLowerCase()); + } + + async update(model: string, extensionId: ExtensionIdentifier, agent: string | undefined, tokenCount: number | undefined): Promise { + await this.extensionFeaturesManagementService.getAccess(extensionId, 'languageModels'); + + // update model access + this.addAccess(model, extensionId.value); + + // update session stats + let sessionStats = this.sessionStats.get(model); + if (!sessionStats) { + sessionStats = { extensions: [] }; + this.sessionStats.set(model, sessionStats); + } + this.add(sessionStats, extensionId.value, agent, tokenCount); + + this.write(model, extensionId.value, agent, tokenCount); + this._onDidChangeStats.fire(model); + } + + private addAccess(model: string, extensionId: string): void { + extensionId = extensionId.toLowerCase(); + const extensions = this.getAccessExtensions(model); + if (!extensions.includes(extensionId)) { + extensions.push(extensionId); + this._storageService.store(this.getAccessKey(model), JSON.stringify(extensions), StorageScope.APPLICATION, StorageTarget.USER); + } + } + + private getAccessExtensions(model: string): string[] { + const key = this.getAccessKey(model); + const data = this._storageService.get(key, StorageScope.APPLICATION); + try { + if (data) { + const parsed = JSON.parse(data); + if (Array.isArray(parsed)) { + return parsed; + } + } + } catch (e) { + // ignore + } + return []; + + } + + private async write(model: string, extensionId: string, participant: string | undefined, tokenCount: number | undefined): Promise { + const modelStats = await this.read(model); + this.add(modelStats, extensionId, participant, tokenCount); + this._storageService.store(this.getKey(model), JSON.stringify(modelStats), StorageScope.APPLICATION, StorageTarget.USER); + } + + private add(modelStats: LanguageModelStats, extensionId: string, participant: string | undefined, tokenCount: number | undefined): void { + let extensionStats = modelStats.extensions.find(e => ExtensionIdentifier.equals(e.extensionId, extensionId)); + if (!extensionStats) { + extensionStats = { extensionId, requestCount: 0, tokenCount: 0, participants: [] }; + modelStats.extensions.push(extensionStats); + } + if (participant) { + let participantStats = extensionStats.participants.find(p => p.id === participant); + if (!participantStats) { + participantStats = { id: participant, requestCount: 0, tokenCount: 0 }; + extensionStats.participants.push(participantStats); + } + participantStats.requestCount++; + participantStats.tokenCount += tokenCount ?? 0; + } else { + extensionStats.requestCount++; + extensionStats.tokenCount += tokenCount ?? 0; + } + } + + private async read(model: string): Promise { + try { + const value = this._storageService.get(this.getKey(model), StorageScope.APPLICATION); + if (value) { + return JSON.parse(value); + } + } catch (error) { + // ignore + } + return { extensions: [] }; + } + + private getModel(key: string): string | undefined { + if (key.startsWith(LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX)) { + return key.substring(LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX.length); + } + return undefined; + } + + private getKey(model: string): string { + return `${LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX}${model}`; + } + + private getAccessKey(model: string): string { + return `${LanguageModelStatsService.MODEL_ACCESS_STORAGE_KEY_PREFIX}${model}`; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'languageModels', + label: localize('Language Models', "Language Models"), + description: localize('languageModels', "Language models usage statistics of this extension."), + access: { + canToggle: false + }, +}); diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts new file mode 100644 index 00000000000..ad5866a8fa4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.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 { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export interface IToolData { + name: string; + icon?: { dark: URI; light?: URI } | ThemeIcon; + displayName?: string; + description: string; + parametersSchema?: IJSONSchema; + canBeInvokedManually?: boolean; +} + +interface IToolEntry { + data: IToolData; + impl?: IToolImpl; +} + +export interface IToolResult { + [contentType: string]: any; + string: string; +} + +export interface IToolImpl { + invoke(parameters: any, token: CancellationToken): Promise; +} + +export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); + +export interface IToolDelta { + added?: IToolData; + removed?: string; +} + +export interface ILanguageModelToolsService { + _serviceBrand: undefined; + onDidChangeTools: Event; + registerToolData(toolData: IToolData): IDisposable; + registerToolImplementation(name: string, tool: IToolImpl): IDisposable; + getTools(): Iterable>; + invokeTool(name: string, parameters: any, token: CancellationToken): Promise; +} + +export class LanguageModelToolsService implements ILanguageModelToolsService { + _serviceBrand: undefined; + + private _onDidChangeTools = new Emitter(); + readonly onDidChangeTools = this._onDidChangeTools.event; + + private _tools = new Map(); + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService + ) { } + + registerToolData(toolData: IToolData): IDisposable { + if (this._tools.has(toolData.name)) { + throw new Error(`Tool "${toolData.name}" is already registered.`); + } + + this._tools.set(toolData.name, { data: toolData }); + this._onDidChangeTools.fire({ added: toolData }); + + return toDisposable(() => { + this._tools.delete(toolData.name); + this._onDidChangeTools.fire({ removed: toolData.name }); + }); + + } + + registerToolImplementation(name: string, tool: IToolImpl): IDisposable { + const entry = this._tools.get(name); + if (!entry) { + throw new Error(`Tool "${name}" was not contributed.`); + } + + if (entry.impl) { + throw new Error(`Tool "${name}" already has an implementation.`); + } + + entry.impl = tool; + return toDisposable(() => { + entry.impl = undefined; + }); + } + + getTools(): Iterable> { + return Iterable.map(this._tools.values(), i => i.data); + } + + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + let tool = this._tools.get(name); + if (!tool) { + throw new Error(`Tool ${name} was not contributed`); + } + + if (!tool.impl) { + await this._extensionService.activateByEvent(`onLanguageModelTool:${name}`); + + // Extension should activate and register the tool implementation + tool = this._tools.get(name); + if (!tool?.impl) { + throw new Error(`Tool ${name} does not have an implementation registered.`); + } + } + + return tool.impl.invoke(parameters, token); + } +} diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 7304d002628..b19a50e5e44 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -5,10 +5,16 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Iterable } from 'vs/base/common/iterator'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IProgress } from 'vs/platform/progress/common/progress'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export const enum ChatMessageRole { System, @@ -16,59 +22,205 @@ export const enum ChatMessageRole { Assistant, } -export interface IChatMessage { - readonly role: ChatMessageRole; - readonly content: string; +export interface IChatMessageTextPart { + type: 'text'; + value: string; } +export interface IChatMessageFunctionResultPart { + type: 'function_result'; + name: string; + value: any; + isError?: boolean; +} + +export type IChatMessagePart = IChatMessageTextPart | IChatMessageFunctionResultPart; + +export interface IChatMessage { + readonly name?: string | undefined; + readonly role: ChatMessageRole; + readonly content: IChatMessagePart; +} + +export interface IChatResponseTextPart { + type: 'text'; + value: string; +} + +export interface IChatResponceFunctionUsePart { + type: 'function_use'; + name: string; + parameters: any; +} + +export type IChatResponsePart = IChatResponseTextPart | IChatResponceFunctionUsePart; + export interface IChatResponseFragment { index: number; - part: string; + part: IChatResponsePart; } export interface ILanguageModelChatMetadata { readonly extension: ExtensionIdentifier; - readonly identifier: string; - readonly model: string; - readonly description?: string; + + readonly name: string; + readonly id: string; + readonly vendor: string; + readonly version: string; + readonly family: string; + readonly maxInputTokens: number; + readonly maxOutputTokens: number; + readonly targetExtensions?: string[]; + readonly auth?: { readonly providerLabel: string; readonly accountLabel?: string; }; } +export interface ILanguageModelChatResponse { + stream: AsyncIterable; + result: Promise; +} + export interface ILanguageModelChat { metadata: ILanguageModelChatMetadata; - provideChatResponse(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; + sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; + provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise; +} + +export interface ILanguageModelChatSelector { + readonly name?: string; + readonly identifier?: string; + readonly vendor?: string; + readonly version?: string; + readonly family?: string; + readonly tokens?: number; + readonly extension?: ExtensionIdentifier; } export const ILanguageModelsService = createDecorator('ILanguageModelsService'); +export interface ILanguageModelsChangeEvent { + added?: { + identifier: string; + metadata: ILanguageModelChatMetadata; + }[]; + removed?: string[]; +} + export interface ILanguageModelsService { readonly _serviceBrand: undefined; - onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>; + onDidChangeLanguageModels: Event; getLanguageModelIds(): string[]; lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined; + selectLanguageModels(selector: ILanguageModelChatSelector): Promise; + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; - makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; + sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; + + computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise; } +const languageModelType: IJSONSchema = { + type: 'object', + properties: { + vendor: { + type: 'string', + description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language models.") + } + } +}; + +interface IUserFriendlyLanguageModel { + vendor: string; +} + +export const languageModelExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'languageModels', + jsonSchema: { + description: localize('vscode.extension.contributes.languageModels', "Contribute language models of a specific vendor."), + oneOf: [ + languageModelType, + { + type: 'array', + items: languageModelType + } + ] + }, + activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => { + for (const contrib of contribs) { + result.push(`onLanguageModelChat:${contrib.vendor}`); + } + } +}); + export class LanguageModelsService implements ILanguageModelsService { + readonly _serviceBrand: undefined; - private readonly _providers: Map = new Map(); + private readonly _store = new DisposableStore(); - private readonly _onDidChangeProviders = new Emitter<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>(); - readonly onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }> = this._onDidChangeProviders.event; + private readonly _providers = new Map(); + private readonly _vendors = new Set(); + + private readonly _onDidChangeProviders = this._store.add(new Emitter()); + readonly onDidChangeLanguageModels: Event = this._onDidChangeProviders.event; + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + @ILogService private readonly _logService: ILogService, + ) { + + this._store.add(languageModelExtensionPoint.setHandler((extensions) => { + + this._vendors.clear(); + + for (const extension of extensions) { + + if (!isProposedApiEnabled(extension.description, 'chatProvider')) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.chatProviderRequired', "This contribution point requires the 'chatProvider' proposal.")); + continue; + } + + for (const item of Iterable.wrap(extension.value)) { + if (this._vendors.has(item.vendor)) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor)); + continue; + } + if (isFalsyOrWhitespace(item.vendor)) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty.")); + continue; + } + if (item.vendor.trim() !== item.vendor) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace.")); + continue; + } + this._vendors.add(item.vendor); + } + } + + const removed: string[] = []; + for (const [identifier, value] of this._providers) { + if (!this._vendors.has(value.metadata.vendor)) { + this._providers.delete(identifier); + removed.push(identifier); + } + } + if (removed.length > 0) { + this._onDidChangeProviders.fire({ removed }); + } + })); + } dispose() { - this._onDidChangeProviders.dispose(); + this._store.dispose(); this._providers.clear(); } @@ -80,24 +232,69 @@ export class LanguageModelsService implements ILanguageModelsService { return this._providers.get(identifier)?.metadata; } + async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { + + if (selector.vendor) { + // selective activation + await this._extensionService.activateByEvent(`onLanguageModelChat:${selector.vendor}}`); + } else { + // activate all extensions that do language models + const all = Array.from(this._vendors).map(vendor => this._extensionService.activateByEvent(`onLanguageModelChat:${vendor}`)); + await Promise.all(all); + } + + const result: string[] = []; + + for (const [identifier, model] of this._providers) { + + if ((selector.vendor === undefined || model.metadata.vendor === selector.vendor) + && (selector.family === undefined || model.metadata.family === selector.family) + && (selector.version === undefined || model.metadata.version === selector.version) + && (selector.identifier === undefined || model.metadata.id === selector.identifier) + && (!model.metadata.targetExtensions || model.metadata.targetExtensions.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension))) + ) { + result.push(identifier); + } + } + + this._logService.trace('[LM] selected language models', selector, result); + + return result; + } + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable { + + this._logService.trace('[LM] registering language model chat', identifier, provider.metadata); + + if (!this._vendors.has(provider.metadata.vendor)) { + throw new Error(`Chat response provider uses UNKNOWN vendor ${provider.metadata.vendor}.`); + } if (this._providers.has(identifier)) { throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); } this._providers.set(identifier, provider); - this._onDidChangeProviders.fire({ added: [provider.metadata] }); + this._onDidChangeProviders.fire({ added: [{ identifier, metadata: provider.metadata }] }); return toDisposable(() => { if (this._providers.delete(identifier)) { this._onDidChangeProviders.fire({ removed: [identifier] }); + this._logService.trace('[LM] UNregistered language model chat', identifier, provider.metadata); } }); } - makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise { + async sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { const provider = this._providers.get(identifier); if (!provider) { throw new Error(`Chat response provider with identifier ${identifier} is not registered.`); } - return provider.provideChatResponse(messages, from, options, progress, token); + return provider.sendChatRequest(messages, from, options, token); + } + + computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise { + const provider = this._providers.get(identifier); + if (!provider) { + throw new Error(`Chat response provider with identifier ${identifier} is not registered.`); + } + return provider.provideTokenCount(message, token); } } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts new file mode 100644 index 00000000000..0c5802da757 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/jsonSchema'; +import { DisposableMap } from 'vs/base/common/lifecycle'; +import { joinPath } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ILanguageModelToolsService, IToolData } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +interface IRawToolContribution { + name: string; + icon?: string | { light: string; dark: string }; + displayName?: string; + description: string; + parametersSchema?: IJSONSchema; + canBeInvokedManually?: boolean; +} + +const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'languageModelTools', + activationEventsGenerator: (contributions: IRawToolContribution[], result) => { + for (const contrib of contributions) { + result.push(`onLanguageModelTool:${contrib.name}`); + } + }, + jsonSchema: { + description: localize('vscode.extension.contributes.tools', 'Contributes a tool that can be invoked by a language model.'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name', 'description'], + properties: { + name: { + description: localize('toolname', "A name for this tool which must be unique across all tools."), + type: 'string' + }, + description: { + description: localize('toolDescription', "A description of this tool that may be passed to a language model."), + type: 'string' + }, + displayName: { + description: localize('toolDisplayName', "A human-readable name for this tool that may be used to describe it in the UI."), + type: 'string' + }, + parametersSchema: { + description: localize('parametersSchema', "A JSON schema for the parameters this tool accepts."), + type: 'object', + $ref: 'http://json-schema.org/draft-07/schema#' + }, + canBeInvokedManually: { + description: localize('canBeInvokedManually', "Whether this tool can be invoked manually by the user through the chat UX."), + type: 'boolean' + }, + icon: { + description: localize('icon', "An icon that represents this tool. Either a file path, an object with file paths for dark and light themes, or a theme icon reference, like `\\$(zap)`"), + anyOf: [{ + type: 'string' + }, + { + type: 'object', + properties: { + light: { + description: localize('icon.light', 'Icon path when a light theme is used'), + type: 'string' + }, + dark: { + description: localize('icon.dark', 'Icon path when a dark theme is used'), + type: 'string' + } + } + }] + } + } + } + } +}); + +function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { + return `${extensionIdentifier.value}/${toolName}`; +} + +export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.toolsExtensionPointHandler'; + + private _registrationDisposables = new DisposableMap(); + + constructor( + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @ILogService logService: ILogService, + ) { + languageModelToolsExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const rawTool of extension.value) { + if (!rawTool.name || !rawTool.description) { + logService.warn(`Invalid tool contribution from ${extension.description.identifier.value}: ${JSON.stringify(rawTool)}`); + continue; + } + + const rawIcon = rawTool.icon; + let icon: IToolData['icon'] | undefined; + if (typeof rawIcon === 'string') { + icon = ThemeIcon.fromString(rawIcon) ?? { + dark: joinPath(extension.description.extensionLocation, rawIcon), + light: joinPath(extension.description.extensionLocation, rawIcon) + }; + } else if (rawIcon) { + icon = { + dark: joinPath(extension.description.extensionLocation, rawIcon.dark), + light: joinPath(extension.description.extensionLocation, rawIcon.light) + }; + } + + const tool = { + ...rawTool, + icon + }; + const disposable = languageModelToolsService.registerToolData(tool); + this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.name), disposable); + } + } + + for (const extension of delta.removed) { + for (const tool of extension.value) { + this._registrationDisposables.deleteAndDispose(toToolKey(extension.description.identifier, tool.name)); + } + } + }); + } +} diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChatService.ts similarity index 79% rename from src/vs/workbench/contrib/chat/common/voiceChat.ts rename to src/vs/workbench/contrib/chat/common/voiceChatService.ts index 1c93007b8cf..ae70f2f56a1 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChatService.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { rtrim } from 'vs/base/common/strings'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -58,6 +60,8 @@ enum PhraseTextType { AGENT_AND_COMMAND = 3 } +export const VoiceChatInProgress = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "A speech-to-text session is in progress for chat.") }); + export class VoiceChatService extends Disposable implements IVoiceChatService { readonly _serviceBrand: undefined; @@ -66,20 +70,24 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private static readonly COMMAND_PREFIX = chatSubcommandLeader; private static readonly PHRASES_LOWER = { - [VoiceChatService.AGENT_PREFIX]: 'at', - [VoiceChatService.COMMAND_PREFIX]: 'slash' + [this.AGENT_PREFIX]: 'at', + [this.COMMAND_PREFIX]: 'slash' }; private static readonly PHRASES_UPPER = { - [VoiceChatService.AGENT_PREFIX]: 'At', - [VoiceChatService.COMMAND_PREFIX]: 'Slash' + [this.AGENT_PREFIX]: 'At', + [this.COMMAND_PREFIX]: 'Slash' }; private static readonly CHAT_AGENT_ALIAS = new Map([['vscode', 'code']]); + private readonly voiceChatInProgress = VoiceChatInProgress.bindTo(this.contextKeyService); + private activeVoiceChatSessions = 0; + constructor( @ISpeechService private readonly speechService: ISpeechService, - @IChatAgentService private readonly chatAgentService: IChatAgentService + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); } @@ -88,15 +96,15 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { const phrases = new Map(); for (const agent of this.chatAgentService.getActivatedAgents()) { - const agentPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]} ${VoiceChatService.CHAT_AGENT_ALIAS.get(agent.id) ?? agent.id}`.toLowerCase(); - phrases.set(agentPhrase, { agent: agent.id }); + const agentPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]} ${VoiceChatService.CHAT_AGENT_ALIAS.get(agent.name) ?? agent.name}`.toLowerCase(); + phrases.set(agentPhrase, { agent: agent.name }); for (const slashCommand of agent.slashCommands) { const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); - phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); + phrases.set(slashCommandPhrase, { agent: agent.name, command: slashCommand.name }); const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); - phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); + phrases.set(agentSlashCommandPhrase, { agent: agent.name, command: slashCommand.name }); } } @@ -116,7 +124,19 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { async createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise { const disposables = new DisposableStore(); - disposables.add(token.onCancellationRequested(() => disposables.dispose())); + + const onSessionStoppedOrCanceled = (dispose: boolean) => { + this.activeVoiceChatSessions = Math.max(0, this.activeVoiceChatSessions - 1); + if (this.activeVoiceChatSessions === 0) { + this.voiceChatInProgress.reset(); + } + + if (dispose) { + disposables.dispose(); + } + }; + + disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled(true))); let detectedAgent = false; let detectedSlashCommand = false; @@ -124,11 +144,16 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { const emitter = disposables.add(new Emitter()); const session = await this.speechService.createSpeechToTextSession(token, 'chat'); + if (token.isCancellationRequested) { + onSessionStoppedOrCanceled(true); + } + const phrases = this.createPhrases(options.model); disposables.add(session.onDidChange(e => { switch (e.status) { case SpeechToTextStatus.Recognizing: - case SpeechToTextStatus.Recognized: + case SpeechToTextStatus.Recognized: { + let massagedEvent: IVoiceChatTextEvent = e; if (e.text) { const startsWithAgent = e.text.startsWith(VoiceChatService.PHRASES_UPPER[VoiceChatService.AGENT_PREFIX]) || e.text.startsWith(VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]); const startsWithSlashCommand = e.text.startsWith(VoiceChatService.PHRASES_UPPER[VoiceChatService.COMMAND_PREFIX]) || e.text.startsWith(VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]); @@ -184,16 +209,26 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { } } - emitter.fire({ + massagedEvent = { status: e.status, text: (transformedWords ?? originalWords).join(' '), waitingForInput - }); - - break; + }; } } - default: + emitter.fire(massagedEvent); + break; + } + case SpeechToTextStatus.Started: + this.activeVoiceChatSessions++; + this.voiceChatInProgress.set(true); + emitter.fire(e); + break; + case SpeechToTextStatus.Stopped: + onSessionStoppedOrCanceled(false); + emitter.fire(e); + break; + case SpeechToTextStatus.Error: emitter.fire(e); break; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css index f386a4a0089..beae62f5939 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -4,16 +4,25 @@ *--------------------------------------------------------------------------------------------*/ /* - * Replace with "microphone" icon. + * Replace "loading" with "microphone" icon. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - content: "\ec1c"; - font-family: 'codicon'; + content: var(--vscode-icon-mic-filled-content); + font-family: var(--vscode-icon-mic-filled-font-family); +} + +/* + * Replace "sync" with "pulse" icon. + */ +.monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before { + content: var(--vscode-icon-pulse-content); + font-family: var(--vscode-icon-pulse-font-family); } /* * Clear animation styles when reduced motion is enabled. */ +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled), .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { animation: none; } @@ -21,7 +30,8 @@ /* * Replace with "stop" icon when reduced motion is enabled. */ +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - content: "\ead7"; - font-family: 'codicon'; + content: var(--vscode-icon-debug-stop-content); + font-family: var(--vscode-icon-debug-stop-font-family); } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 0e2bd19e8d5..15e2584f498 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -4,80 +4,94 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/voiceChatActions'; -import { Event } from 'vs/base/common/event'; -import { firstOrDefault } from 'vs/base/common/arrays'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { RunOnceScheduler, disposableTimeout, raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; +import { Color } from 'vs/base/common/color'; +import { Event } from 'vs/base/common/event'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isNumber } from 'vs/base/common/types'; +import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; -import { IChatService, KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; -import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; -import { RunOnceScheduler, disposableTimeout } from 'vs/base/common/async'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { Color } from 'vs/base/common/color'; -import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { assertIsDefined, isNumber } from 'vs/base/common/types'; -import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ProgressLocation } from 'vs/platform/progress/common/progress'; -import { TerminalChatController, TerminalChatContextKeys } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; +import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IVoiceChatService, VoiceChatInProgress as GlobalVoiceChatInProgress } from 'vs/workbench/contrib/chat/common/voiceChatService'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; +import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechStatus, TextToSpeechInProgress as GlobalTextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalChatContextKeys, TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; -const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") }); -const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") }); +//#region Speech to Text -const CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS = new RawContextKey('quickVoiceChatInProgress', false, { type: 'boolean', description: localize('quickVoiceChatInProgress', "True when voice recording from microphone is in progress for quick chat.") }); -const CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS = new RawContextKey('inlineVoiceChatInProgress', false, { type: 'boolean', description: localize('inlineVoiceChatInProgress', "True when voice recording from microphone is in progress for inline chat.") }); -const CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS = new RawContextKey('terminalVoiceChatInProgress', false, { type: 'boolean', description: localize('terminalVoiceChatInProgress', "True when voice recording from microphone is in progress for terminal chat.") }); -const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voiceChatInViewInProgress', false, { type: 'boolean', description: localize('voiceChatInViewInProgress', "True when voice recording from microphone is in progress in the chat view.") }); -const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); +type VoiceChatSessionContext = 'view' | 'inline' | 'terminal' | 'quick' | 'editor'; +const VoiceChatSessionContexts: VoiceChatSessionContext[] = ['view', 'inline', 'terminal', 'quick', 'editor']; -const CanVoiceChat = ContextKeyExpr.and(CONTEXT_PROVIDER_EXISTS, HasSpeechProvider); -const FocusInChatInput = assertIsDefined(ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT)); +const TerminalChatExecute = MenuId.for('terminalChatInput'); // unfortunately, terminal decided to go with their own menu (https://github.com/microsoft/vscode/issues/208789) -type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; +// Global Context Keys (set on global context key service) +const CanVoiceChat = ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, HasSpeechProvider); +const FocusInChatInput = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT); +const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, TerminalChatContextKeys.requestActive); + +// Scoped Context Keys (set on per-chat-context scoped context key service) +const ScopedVoiceChatGettingReady = new RawContextKey('scopedVoiceChatGettingReady', false, { type: 'boolean', description: localize('scopedVoiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat. This key is only defined scoped, per chat context.") }); +const ScopedVoiceChatInProgress = new RawContextKey('scopedVoiceChatInProgress', undefined, { type: 'string', description: localize('scopedVoiceChatInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); +const AnyScopedVoiceChatInProgress = ContextKeyExpr.or(...VoiceChatSessionContexts.map(context => ScopedVoiceChatInProgress.isEqualTo(context))); + +enum VoiceChatSessionState { + Stopped = 1, + GettingReady, + Started +} interface IVoiceChatSessionController { readonly onDidAcceptInput: Event; - readonly onDidCancelInput: Event; + readonly onDidHideInput: Event; readonly context: VoiceChatSessionContext; + readonly scopedContextKeyService: IContextKeyService; + + updateState(state: VoiceChatSessionState): void; focusInput(): void; - acceptInput(): void; + acceptInput(): Promise; updateInput(text: string): void; getInput(): string; @@ -87,187 +101,137 @@ interface IVoiceChatSessionController { class VoiceChatSessionControllerFactory { - static create(accessor: ServicesAccessor, context: 'inline'): Promise; - static create(accessor: ServicesAccessor, context: 'quick'): Promise; - static create(accessor: ServicesAccessor, context: 'view'): Promise; - static create(accessor: ServicesAccessor, context: 'focused'): Promise; - static create(accessor: ServicesAccessor, context: 'terminal'): Promise; - static create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise; - static async create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise { + static async create(accessor: ServicesAccessor, context: 'view' | 'inline' | 'quick' | 'focused'): Promise { const chatWidgetService = accessor.get(IChatWidgetService); - const viewsService = accessor.get(IViewsService); - const chatContributionService = accessor.get(IChatContributionService); const quickChatService = accessor.get(IQuickChatService); const layoutService = accessor.get(IWorkbenchLayoutService); const editorService = accessor.get(IEditorService); const terminalService = accessor.get(ITerminalService); + const viewsService = accessor.get(IViewsService); - // Currently Focused Context - if (context === 'focused') { - - // Try with the terminal chat - const activeInstance = terminalService.activeInstance; - if (activeInstance) { - const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - if (terminalChat?.hasFocus()) { - return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); - } + switch (context) { + case 'focused': { + const controller = VoiceChatSessionControllerFactory.doCreateForFocusedChat(terminalService, chatWidgetService, layoutService); + return controller ?? VoiceChatSessionControllerFactory.create(accessor, 'view'); // fallback to 'view' } - - // Try with the chat widget service, which currently - // only supports the chat view and quick chat - // https://github.com/microsoft/vscode/issues/191191 - const chatInput = chatWidgetService.lastFocusedWidget; - if (chatInput?.hasInputFocus()) { - // Unfortunately there does not seem to be a better way - // to figure out if the chat widget is in a part or picker - if ( - layoutService.hasFocus(Parts.SIDEBAR_PART) || - layoutService.hasFocus(Parts.PANEL_PART) || - layoutService.hasFocus(Parts.AUXILIARYBAR_PART) - ) { - return VoiceChatSessionControllerFactory.doCreateForChatView(chatInput, viewsService, chatContributionService); + case 'view': { + const chatWidget = await showChatView(viewsService); + if (chatWidget) { + return VoiceChatSessionControllerFactory.doCreateForChatWidget('view', chatWidget); } - - if (layoutService.hasFocus(Parts.EDITOR_PART)) { - return VoiceChatSessionControllerFactory.doCreateForChatEditor(chatInput, viewsService, chatContributionService); + break; + } + case 'inline': { + const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); + if (activeCodeEditor) { + const inlineChat = InlineChatController.get(activeCodeEditor); + if (inlineChat) { + if (!inlineChat.joinCurrentRun()) { + inlineChat.run(); + } + return VoiceChatSessionControllerFactory.doCreateForChatWidget('inline', inlineChat.chatWidget); + } } - - return VoiceChatSessionControllerFactory.doCreateForQuickChat(chatInput, quickChatService); + break; } - - // Try with the inline chat - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor) { - const inlineChat = InlineChatController.get(activeCodeEditor); - if (inlineChat?.hasFocus()) { - return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); - } - } - } - - // View Chat - if (context === 'view' || context === 'focused' /* fallback in case 'focused' was not successful */) { - const chatView = await VoiceChatSessionControllerFactory.revealChatView(accessor); - if (chatView) { - return VoiceChatSessionControllerFactory.doCreateForChatView(chatView, viewsService, chatContributionService); - } - } - - // Inline Chat - if (context === 'inline') { - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor) { - const inlineChat = InlineChatController.get(activeCodeEditor); - if (inlineChat) { - return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); - } - } - } - - // Terminal Chat - if (context === 'terminal') { - const activeInstance = terminalService.activeInstance; - if (activeInstance) { - const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - if (terminalChat) { - return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); - } - } - } - - // Quick Chat - if (context === 'quick') { - quickChatService.open(); - - const quickChat = chatWidgetService.lastFocusedWidget; - if (quickChat) { - return VoiceChatSessionControllerFactory.doCreateForQuickChat(quickChat, quickChatService); + case 'quick': { + quickChatService.open(); // this will populate focused chat widget in the chat widget service + return VoiceChatSessionControllerFactory.create(accessor, 'focused'); } } return undefined; } - static async revealChatView(accessor: ServicesAccessor): Promise { - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); + private static doCreateForFocusedChat(terminalService: ITerminalService, chatWidgetService: IChatWidgetService, layoutService: IWorkbenchLayoutService): IVoiceChatSessionController | undefined { - const provider = firstOrDefault(chatService.getProviderInfos()); - if (provider) { - return chatWidgetService.revealViewForProvider(provider.id); + // 1.) probe terminal chat which is not part of chat widget service + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); + } + } + + // 2.) otherwise go via chat widget service + const chatWidget = chatWidgetService.lastFocusedWidget; + if (chatWidget?.hasInputFocus()) { + + // Figure out the context of the chat widget by asking + // layout service for the part that has focus. Unfortunately + // there is no better way because the widget does not know + // its location. + + let context: VoiceChatSessionContext; + if (layoutService.hasFocus(Parts.EDITOR_PART)) { + context = chatWidget.location === ChatAgentLocation.Panel ? 'editor' : 'inline'; + } else if ( + [Parts.SIDEBAR_PART, Parts.PANEL_PART, Parts.AUXILIARYBAR_PART, Parts.TITLEBAR_PART, Parts.STATUSBAR_PART, Parts.BANNER_PART, Parts.ACTIVITYBAR_PART].some(part => layoutService.hasFocus(part)) + ) { + context = 'view'; + } else { + context = 'quick'; + } + + return VoiceChatSessionControllerFactory.doCreateForChatWidget(context, chatWidget); } return undefined; } - private static doCreateForChatView(chatView: IChatWidget, viewsService: IViewsService, chatContributionService: IChatContributionService): IVoiceChatSessionController { - return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('view', chatView, viewsService, chatContributionService); + private static createChatContextKeyController(contextKeyService: IContextKeyService, context: VoiceChatSessionContext): (state: VoiceChatSessionState) => void { + const contextVoiceChatGettingReady = ScopedVoiceChatGettingReady.bindTo(contextKeyService); + const contextVoiceChatInProgress = ScopedVoiceChatInProgress.bindTo(contextKeyService); + + return (state: VoiceChatSessionState) => { + switch (state) { + case VoiceChatSessionState.GettingReady: + contextVoiceChatGettingReady.set(true); + contextVoiceChatInProgress.reset(); + break; + case VoiceChatSessionState.Started: + contextVoiceChatGettingReady.reset(); + contextVoiceChatInProgress.set(context); + break; + case VoiceChatSessionState.Stopped: + contextVoiceChatGettingReady.reset(); + contextVoiceChatInProgress.reset(); + break; + } + }; } - private static doCreateForChatEditor(chatView: IChatWidget, viewsService: IViewsService, chatContributionService: IChatContributionService): IVoiceChatSessionController { - return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('editor', chatView, viewsService, chatContributionService); - } - - private static doCreateForChatViewOrEditor(context: 'view' | 'editor', chatView: IChatWidget, viewsService: IViewsService, chatContributionService: IChatContributionService): IVoiceChatSessionController { + private static doCreateForChatWidget(context: VoiceChatSessionContext, chatWidget: IChatWidget): IVoiceChatSessionController { return { context, - onDidAcceptInput: chatView.onDidAcceptInput, - // TODO@bpasero cancellation needs to work better for chat editors that are not view bound - onDidCancelInput: Event.filter(viewsService.onDidChangeViewVisibility, e => e.id === chatContributionService.getViewIdForProvider(chatView.providerId)), - focusInput: () => chatView.focusInput(), - acceptInput: () => chatView.acceptInput(), - updateInput: text => chatView.setInput(text), - getInput: () => chatView.getInput(), - setInputPlaceholder: text => chatView.setInputPlaceholder(text), - clearInputPlaceholder: () => chatView.resetInputPlaceholder() - }; - } - - private static doCreateForQuickChat(quickChat: IChatWidget, quickChatService: IQuickChatService): IVoiceChatSessionController { - return { - context: 'quick', - onDidAcceptInput: quickChat.onDidAcceptInput, - onDidCancelInput: quickChatService.onDidClose, - focusInput: () => quickChat.focusInput(), - acceptInput: () => quickChat.acceptInput(), - updateInput: text => quickChat.setInput(text), - getInput: () => quickChat.getInput(), - setInputPlaceholder: text => quickChat.setInputPlaceholder(text), - clearInputPlaceholder: () => quickChat.resetInputPlaceholder() - }; - } - - private static doCreateForInlineChat(inlineChat: InlineChatController): IVoiceChatSessionController { - const inlineChatSession = inlineChat.joinCurrentRun() ?? inlineChat.run(); - - return { - context: 'inline', - onDidAcceptInput: inlineChat.onDidAcceptInput, - onDidCancelInput: Event.any( - inlineChat.onDidCancelInput, - Event.fromPromise(inlineChatSession) - ), - focusInput: () => inlineChat.focus(), - acceptInput: () => inlineChat.acceptInput(), - updateInput: text => inlineChat.updateInput(text, false), - getInput: () => inlineChat.getInput(), - setInputPlaceholder: text => inlineChat.setPlaceholder(text), - clearInputPlaceholder: () => inlineChat.resetPlaceholder() + scopedContextKeyService: chatWidget.scopedContextKeyService, + onDidAcceptInput: chatWidget.onDidAcceptInput, + onDidHideInput: chatWidget.onDidHide, + focusInput: () => chatWidget.focusInput(), + acceptInput: () => chatWidget.acceptInput(), + updateInput: text => chatWidget.setInput(text), + getInput: () => chatWidget.getInput(), + setInputPlaceholder: text => chatWidget.setInputPlaceholder(text), + clearInputPlaceholder: () => chatWidget.resetInputPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createChatContextKeyController(chatWidget.scopedContextKeyService, context) }; } private static doCreateForTerminalChat(terminalChat: TerminalChatController): IVoiceChatSessionController { + const context = 'terminal'; return { - context: 'terminal', + context, + scopedContextKeyService: terminalChat.scopedContextKeyService, onDidAcceptInput: terminalChat.onDidAcceptInput, - onDidCancelInput: terminalChat.onDidCancelInput, + onDidHideInput: terminalChat.onDidHide, focusInput: () => terminalChat.focus(), acceptInput: () => terminalChat.acceptInput(), updateInput: text => terminalChat.updateInput(text, false), getInput: () => terminalChat.getInput(), setInputPlaceholder: text => terminalChat.setPlaceholder(text), - clearInputPlaceholder: () => terminalChat.resetPlaceholder() + clearInputPlaceholder: () => terminalChat.resetPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createChatContextKeyController(terminalChat.scopedContextKeyService, context) }; } } @@ -296,26 +260,21 @@ class VoiceChatSessions { return VoiceChatSessions.instance; } - private voiceChatInProgressKey = CONTEXT_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatGettingReadyKey = CONTEXT_VOICE_CHAT_GETTING_READY.bindTo(this.contextKeyService); - - private quickVoiceChatInProgressKey = CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private inlineVoiceChatInProgressKey = CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private terminalVoiceChatInProgressKey = CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatInViewInProgressKey = CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatInEditorInProgressKey = CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.bindTo(this.contextKeyService); - private currentVoiceChatSession: IActiveVoiceChatSession | undefined = undefined; private voiceChatSessionIds = 0; constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IVoiceChatService private readonly voiceChatService: IVoiceChatService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { } async start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): Promise { + + // Stop running text-to-speech or speech-to-text sessions in chats this.stop(); + ChatSynthesizerSessions.getInstance(this.instantiationService).stop(); let disableTimeout = false; @@ -325,7 +284,7 @@ class VoiceChatSessions { controller, disposables: new DisposableStore(), setTimeoutDisabled: (disabled: boolean) => { disableTimeout = disabled; }, - accept: () => session.controller.acceptInput(), + accept: () => this.accept(sessionId), stop: () => this.stop(sessionId, controller.context) }; @@ -333,11 +292,11 @@ class VoiceChatSessions { session.disposables.add(toDisposable(() => cts.dispose(true))); session.disposables.add(controller.onDidAcceptInput(() => this.stop(sessionId, controller.context))); - session.disposables.add(controller.onDidCancelInput(() => this.stop(sessionId, controller.context))); + session.disposables.add(controller.onDidHideInput(() => this.stop(sessionId, controller.context))); controller.focusInput(); - this.voiceChatGettingReadyKey.set(true); + controller.updateState(VoiceChatSessionState.GettingReady); const voiceChatSession = await this.voiceChatService.createVoiceChatSession(cts.token, { usesAgents: controller.context !== 'inline', model: context?.widget?.viewModel?.model }); @@ -348,7 +307,7 @@ class VoiceChatSessions { voiceChatTimeout = SpeechTimeoutDefault; } - const acceptTranscriptionScheduler = session.disposables.add(new RunOnceScheduler(() => session.controller.acceptInput(), voiceChatTimeout)); + const acceptTranscriptionScheduler = session.disposables.add(new RunOnceScheduler(() => this.accept(sessionId), voiceChatTimeout)); session.disposables.add(voiceChatSession.onDidChange(({ status, text, waitingForInput }) => { if (cts.token.isCancellationRequested) { return; @@ -385,26 +344,7 @@ class VoiceChatSessions { } private onDidSpeechToTextSessionStart(controller: IVoiceChatSessionController, disposables: DisposableStore): void { - this.voiceChatGettingReadyKey.set(false); - this.voiceChatInProgressKey.set(true); - - switch (controller.context) { - case 'inline': - this.inlineVoiceChatInProgressKey.set(true); - break; - case 'terminal': - this.terminalVoiceChatInProgressKey.set(true); - break; - case 'quick': - this.quickVoiceChatInProgressKey.set(true); - break; - case 'view': - this.voiceChatInViewInProgressKey.set(true); - break; - case 'editor': - this.voiceChatInEditorInProgressKey.set(true); - break; - } + controller.updateState(VoiceChatSessionState.Started); let dotCount = 0; @@ -429,20 +369,13 @@ class VoiceChatSessions { this.currentVoiceChatSession.controller.clearInputPlaceholder(); + this.currentVoiceChatSession.controller.updateState(VoiceChatSessionState.Stopped); + this.currentVoiceChatSession.disposables.dispose(); this.currentVoiceChatSession = undefined; - - this.voiceChatGettingReadyKey.set(false); - this.voiceChatInProgressKey.set(false); - - this.quickVoiceChatInProgressKey.set(false); - this.inlineVoiceChatInProgressKey.set(false); - this.terminalVoiceChatInProgressKey.set(false); - this.voiceChatInViewInProgressKey.set(false); - this.voiceChatInEditorInProgressKey.set(false); } - accept(voiceChatSessionId = this.voiceChatSessionIds): void { + async accept(voiceChatSessionId = this.voiceChatSessionIds): Promise { if ( !this.currentVoiceChatSession || this.voiceChatSessionIds !== voiceChatSessionId @@ -450,13 +383,30 @@ class VoiceChatSessions { return; } - this.currentVoiceChatSession.controller.acceptInput(); + const controller = this.currentVoiceChatSession.controller; + const response = await controller.acceptInput(); + if (!response) { + return; + } + const autoSynthesize = this.configurationService.getValue<'on' | 'off' | 'auto'>(AccessibilityVoiceSettingId.AutoSynthesize); + if (autoSynthesize === 'on' || autoSynthesize === 'auto' && !this.accessibilityService.isScreenReaderOptimized()) { + let context: IVoiceChatSessionController | 'focused'; + if (controller.context === 'inline') { + // TODO@bpasero this is ugly, but the lightweight inline chat turns into + // a different widget as soon as a response comes in, so we fallback to + // picking up from the focused chat widget + context = 'focused'; + } else { + context = controller; + } + ChatSynthesizerSessions.getInstance(this.instantiationService).start(this.instantiationService.invokeFunction(accessor => ChatSynthesizerSessionController.create(accessor, context, response))); + } } } export const VOICE_KEY_HOLD_THRESHOLD = 500; -async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor, target: 'inline' | 'quick' | 'view' | 'focused', context?: IChatExecuteActionContext): Promise { +async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor, target: 'view' | 'inline' | 'quick' | 'focused', context?: IChatExecuteActionContext): Promise { const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); @@ -484,7 +434,7 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor class VoiceChatWithHoldModeAction extends Action2 { - constructor(desc: Readonly, private readonly target: 'inline' | 'quick' | 'view') { + constructor(desc: Readonly, private readonly target: 'view' | 'inline' | 'quick') { super(desc); } @@ -500,9 +450,12 @@ export class VoiceChatInChatViewAction extends VoiceChatWithHoldModeAction { constructor() { super({ id: VoiceChatInChatViewAction.ID, - title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in View"), + title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in Chat View"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'view'); } @@ -515,14 +468,15 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { constructor() { super({ id: HoldToVoiceChatInChatViewAction.ID, - title: localize2('workbench.action.chat.holdToVoiceChatInChatView.label', "Hold to Voice Chat in View"), + title: localize2('workbench.action.chat.holdToVoiceChatInChatView.label', "Hold to Voice Chat in Chat View"), keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( CanVoiceChat, - FocusInChatInput.negate(), // when already in chat input, disable this action and prefer to start voice chat directly - EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook keybinding + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), // disable when a chat request is in progress + FocusInChatInput?.negate(), // when already in chat input, disable this action and prefer to start voice chat directly + EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI } @@ -537,6 +491,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); + const viewsService = accessor.get(IViewsService); const holdMode = keybindingService.enableKeybindingHoldMode(HoldToVoiceChatInChatViewAction.ID); @@ -549,7 +504,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { } }, VOICE_KEY_HOLD_THRESHOLD); - (await VoiceChatSessionControllerFactory.revealChatView(accessor))?.focusInput(); + (await showChatView(viewsService))?.focusInput(); await holdMode; handle.dispose(); @@ -569,7 +524,11 @@ export class InlineVoiceChatAction extends VoiceChatWithHoldModeAction { id: InlineVoiceChatAction.ID, title: localize2('workbench.action.chat.inlineVoiceChat', "Inline Voice Chat"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, ActiveEditorContext, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + ActiveEditorContext, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'inline'); } @@ -584,7 +543,10 @@ export class QuickVoiceChatAction extends VoiceChatWithHoldModeAction { id: QuickVoiceChatAction.ID, title: localize2('workbench.action.chat.quickVoiceChat.label', "Quick Voice Chat"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'quick'); } @@ -604,26 +566,34 @@ export class StartVoiceChatAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( FocusInChatInput, // scope this action to chat input fields only - EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate(), // do not steal the notebook keybinding - CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), - CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), - CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), - CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate(), - CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate() + EditorContextKeys.focus.negate(), // do not steal the editor inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook inline-chat keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, icon: Codicon.mic, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_GETTING_READY.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.negate(), TerminalChatContextKeys.requestActive.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + ScopedVoiceChatGettingReady.negate(), // disable when voice chat is getting ready + AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress + SpeechToTextInProgress.negate() // disable when speech to text is in progress + ), menu: [{ id: MenuId.ChatExecute, - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate()), + when: ContextKeyExpr.and( + HasSpeechProvider, + ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress + AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress + ), group: 'navigation', order: -1 }, { - id: MenuId.for('terminalChatInput'), - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate()), + id: TerminalChatExecute, + when: ContextKeyExpr.and( + HasSpeechProvider, + ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress + AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress + ), group: 'navigation', order: -1 }] @@ -637,9 +607,6 @@ export class StartVoiceChatAction extends Action2 { // from a toolbar within the chat widget, then make sure // to move focus into the input field so that the controller // is properly retrieved - // TODO@bpasero this will actually not work if the button - // is clicked from the inline editor while focus is in a - // chat input field in a view or picker widget.focusInput(); } @@ -647,29 +614,31 @@ export class StartVoiceChatAction extends Action2 { } } -const InstallingSpeechProvider = new RawContextKey('installingSpeechProvider', false, true); +export class StopListeningAction extends Action2 { -export class InstallVoiceChatAction extends Action2 { - - static readonly ID = 'workbench.action.chat.installVoiceChat'; - - private static readonly SPEECH_EXTENSION_ID = 'ms-vscode.vscode-speech'; + static readonly ID = 'workbench.action.chat.stopListening'; constructor() { super({ - id: InstallVoiceChatAction.ID, - title: localize2('workbench.action.chat.startVoiceChat.label', "Start Voice Chat"), + id: StopListeningAction.ID, + title: localize2('workbench.action.chat.stopListening.label', "Stop Listening"), category: CHAT_CATEGORY, - icon: Codicon.mic, - precondition: InstallingSpeechProvider.negate(), + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape, + when: AnyScopedVoiceChatInProgress + }, + icon: spinningLoading, + precondition: GlobalVoiceChatInProgress, // need global context here because of `f1: true` menu: [{ id: MenuId.ChatExecute, - when: HasSpeechProvider.negate(), + when: AnyScopedVoiceChatInProgress, group: 'navigation', order: -1 }, { - id: MenuId.for('terminalChatInput'), - when: HasSpeechProvider.negate(), + id: TerminalChatExecute, + when: AnyScopedVoiceChatInProgress, group: 'navigation', order: -1 }] @@ -677,93 +646,7 @@ export class InstallVoiceChatAction extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const contextKeyService = accessor.get(IContextKeyService); - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - try { - InstallingSpeechProvider.bindTo(contextKeyService).set(true); - await extensionsWorkbenchService.install(InstallVoiceChatAction.SPEECH_EXTENSION_ID, { - justification: localize('confirmInstallDetail', "Microphone support requires this extension."), - enable: true - }, ProgressLocation.Notification); - } finally { - InstallingSpeechProvider.bindTo(contextKeyService).set(false); - } - } -} - -class BaseStopListeningAction extends Action2 { - - constructor( - desc: { id: string; icon?: ThemeIcon; f1?: boolean }, - private readonly target: 'inline' | 'terminal' | 'quick' | 'view' | 'editor' | undefined, - context: RawContextKey, - menu: MenuId | undefined, - ) { - super({ - ...desc, - title: localize2('workbench.action.chat.stopListening.label', "Stop Listening"), - category: CHAT_CATEGORY, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib + 100, - primary: KeyCode.Escape - }, - precondition: ContextKeyExpr.and(CanVoiceChat, context), - menu: menu ? [{ - id: menu, - when: ContextKeyExpr.and(CanVoiceChat, context), - group: 'navigation', - order: -1 - }] : undefined - }); - } - - async run(accessor: ServicesAccessor, context?: IChatExecuteActionContext): Promise { - VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(undefined, this.target); - } -} - -export class StopListeningAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListening'; - - constructor() { - super({ id: StopListeningAction.ID, f1: true }, undefined, CONTEXT_VOICE_CHAT_IN_PROGRESS, undefined); - } -} - -export class StopListeningInChatViewAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInChatView'; - - constructor() { - super({ id: StopListeningInChatViewAction.ID, icon: spinningLoading }, 'view', CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInChatEditorAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInChatEditor'; - - constructor() { - super({ id: StopListeningInChatEditorAction.ID, icon: spinningLoading }, 'editor', CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInQuickChatAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInQuickChat'; - - constructor() { - super({ id: StopListeningInQuickChatAction.ID, icon: spinningLoading }, 'quick', CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInTerminalChatAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInTerminalChat'; - - constructor() { - super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, 'terminal', CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS, MenuId.for('terminalChatInput')); + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(); } } @@ -779,10 +662,13 @@ export class StopListeningAndSubmitAction extends Action2 { f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: FocusInChatInput, + when: ContextKeyExpr.and( + FocusInChatInput, + AnyScopedVoiceChatInProgress + ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_IN_PROGRESS) + precondition: GlobalVoiceChatInProgress // need global context here because of `f1: true` }); } @@ -791,68 +677,310 @@ export class StopListeningAndSubmitAction extends Action2 { } } -registerThemingParticipant((theme, collector) => { - let activeRecordingColor: Color | undefined; - let activeRecordingDimmedColor: Color | undefined; - if (theme.type === ColorScheme.LIGHT || theme.type === ColorScheme.DARK) { - activeRecordingColor = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND) ?? theme.getColor(focusBorder); - activeRecordingDimmedColor = activeRecordingColor?.transparent(0.38); - } else { - activeRecordingColor = theme.getColor(contrastBorder); - activeRecordingDimmedColor = theme.getColor(contrastBorder); +//#endregion + +//#region Text to Speech + +const ScopedChatSynthesisInProgress = new RawContextKey('scopedChatSynthesisInProgress', false, { type: 'boolean', description: localize('scopedChatSynthesisInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); + +interface IChatSynthesizerSessionController { + + readonly onDidHideChat: Event; + + readonly contextKeyService: IContextKeyService; + readonly response: IChatResponseModel; +} + +class ChatSynthesizerSessionController { + + static create(accessor: ServicesAccessor, context: IVoiceChatSessionController | 'focused', response: IChatResponseModel): IChatSynthesizerSessionController { + if (context === 'focused') { + return ChatSynthesizerSessionController.doCreateForFocusedChat(accessor, response); + } else { + return { + onDidHideChat: context.onDidHideInput, + contextKeyService: context.scopedContextKeyService, + response + }; + } } - // Show a "microphone" icon when recording is in progress that glows via outline. - collector.addRule(` - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { - color: ${activeRecordingColor}; - outline: 1px solid ${activeRecordingColor}; - outline-offset: -1px; - animation: pulseAnimation 1s infinite; - border-radius: 50%; - } + private static doCreateForFocusedChat(accessor: ServicesAccessor, response: IChatResponseModel): IChatSynthesizerSessionController { + const chatWidgetService = accessor.get(IChatWidgetService); + const contextKeyService = accessor.get(IContextKeyService); + const terminalService = accessor.get(ITerminalService); - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - position: absolute; - outline: 1px solid ${activeRecordingColor}; - outline-offset: 2px; - border-radius: 50%; - width: 16px; - height: 16px; - } - - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { - outline: 2px solid ${activeRecordingColor}; - outline-offset: -1px; - animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; - } - - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - position: absolute; - outline: 1px solid ${activeRecordingColor}; - outline-offset: 2px; - border-radius: 50%; - width: 16px; - height: 16px; - } - - @keyframes pulseAnimation { - 0% { - outline-width: 2px; - } - 62% { - outline-width: 5px; - outline-color: ${activeRecordingDimmedColor}; - } - 100% { - outline-width: 2px; + // 1.) probe terminal chat which is not part of chat widget service + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return { + onDidHideChat: terminalChat.onDidHide, + contextKeyService: terminalChat.scopedContextKeyService, + response + }; } } - `); -}); -function supportsKeywordActivation(configurationService: IConfigurationService, speechService: ISpeechService): boolean { - if (!speechService.hasSpeechProvider) { + // 2.) otherwise go via chat widget service + let chatWidget = chatWidgetService.getWidgetBySessionId(response.session.sessionId); + if (chatWidget?.location === ChatAgentLocation.Editor) { + // TODO@bpasero workaround for https://github.com/microsoft/vscode/issues/212785 + chatWidget = chatWidgetService.lastFocusedWidget; + } + + return { + onDidHideChat: chatWidget?.onDidHide ?? Event.None, + contextKeyService: chatWidget?.scopedContextKeyService ?? contextKeyService, + response + }; + } +} + +class ChatSynthesizerSessions { + + private static instance: ChatSynthesizerSessions | undefined = undefined; + static getInstance(instantiationService: IInstantiationService): ChatSynthesizerSessions { + if (!ChatSynthesizerSessions.instance) { + ChatSynthesizerSessions.instance = instantiationService.createInstance(ChatSynthesizerSessions); + } + + return ChatSynthesizerSessions.instance; + } + + private activeSession: CancellationTokenSource | undefined = undefined; + + constructor( + @ISpeechService private readonly speechService: ISpeechService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async start(controller: IChatSynthesizerSessionController): Promise { + + // Stop running text-to-speech or speech-to-text sessions in chats + this.stop(); + VoiceChatSessions.getInstance(this.instantiationService).stop(); + + const activeSession = this.activeSession = new CancellationTokenSource(); + + const disposables = new DisposableStore(); + activeSession.token.onCancellationRequested(() => disposables.dispose()); + + const session = await this.speechService.createTextToSpeechSession(activeSession.token, 'chat'); + + if (activeSession.token.isCancellationRequested) { + return; + } + + disposables.add(controller.onDidHideChat(() => this.stop())); + + const scopedChatToSpeechInProgress = ScopedChatSynthesisInProgress.bindTo(controller.contextKeyService); + disposables.add(toDisposable(() => scopedChatToSpeechInProgress.reset())); + + disposables.add(session.onDidChange(e => { + switch (e.status) { + case TextToSpeechStatus.Started: + scopedChatToSpeechInProgress.set(true); + break; + case TextToSpeechStatus.Stopped: + scopedChatToSpeechInProgress.reset(); + break; + } + })); + + for await (const chunk of this.nextChatResponseChunk(controller.response, activeSession.token)) { + if (activeSession.token.isCancellationRequested) { + return; + } + + await raceCancellation(session.synthesize(chunk), activeSession.token); + } + } + + private async *nextChatResponseChunk(response: IChatResponseModel, token: CancellationToken): AsyncIterable { + let totalOffset = 0; + let complete = false; + do { + const responseLength = response.response.toString().length; + const { chunk, offset } = this.parseNextChatResponseChunk(response, totalOffset); + totalOffset = offset; + complete = response.isComplete; + + if (chunk) { + yield chunk; + } + + if (token.isCancellationRequested) { + return; + } + + if (!complete && responseLength === response.response.toString().length) { + await raceCancellation(Event.toPromise(response.onDidChange), token); // wait for the response to change + } + } while (!token.isCancellationRequested && !complete); + } + + private parseNextChatResponseChunk(response: IChatResponseModel, offset: number): { readonly chunk: string | undefined; readonly offset: number } { + let chunk: string | undefined = undefined; + + const text = response.response.toString(); + + if (response.isComplete) { + chunk = text.substring(offset); + offset = text.length + 1; + } else { + const res = parseNextChatResponseChunk(text, offset); + chunk = res.chunk; + offset = res.offset; + } + + return { + chunk: chunk ? renderStringAsPlaintext({ value: chunk }) : chunk, // convert markdown to plain text + offset + }; + } + + stop(): void { + this.activeSession?.dispose(true); + this.activeSession = undefined; + } +} + +const sentenceDelimiter = ['.', '!', '?', ':']; +const lineDelimiter = '\n'; +const wordDelimiter = ' '; + +export function parseNextChatResponseChunk(text: string, offset: number): { readonly chunk: string | undefined; readonly offset: number } { + let chunk: string | undefined = undefined; + + for (let i = text.length - 1; i >= offset; i--) { // going from end to start to produce largest chunks + const cur = text[i]; + const next = text[i + 1]; + if ( + sentenceDelimiter.includes(cur) && next === wordDelimiter || // end of sentence + lineDelimiter === cur // end of line + ) { + chunk = text.substring(offset, i + 1).trim(); + offset = i + 1; + break; + } + } + + return { chunk, offset }; +} + +export class ReadChatResponseAloud extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.readChatResponseAloud', + title: localize2('workbench.action.chat.readChatResponseAloud', "Read Aloud"), + icon: Codicon.unmute, + precondition: CanVoiceChat, + menu: { + id: MenuId.ChatMessageTitle, + when: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_RESPONSE, // only for responses + ScopedChatSynthesisInProgress.negate(), // but not when already in progress + CONTEXT_RESPONSE_FILTERED.negate() // and not when response is filtered + ), + group: 'navigation' + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const instantiationService = accessor.get(IInstantiationService); + + const response = args[0]; + if (!isResponseVM(response)) { + return; + } + + const controller = ChatSynthesizerSessionController.create(accessor, 'focused', response.model); + ChatSynthesizerSessions.getInstance(instantiationService).start(controller); + } +} + +export class StopReadAloud extends Action2 { + + static readonly ID = 'workbench.action.speech.stopReadAloud'; + + constructor() { + super({ + id: StopReadAloud.ID, + icon: syncing, + title: localize2('workbench.action.speech.stopReadAloud', "Stop Reading Aloud"), + f1: true, + category: CHAT_CATEGORY, + precondition: GlobalTextToSpeechInProgress, // need global context here because of `f1: true` + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape, + when: ScopedChatSynthesisInProgress + }, + menu: [ + { + id: MenuId.ChatExecute, + when: ScopedChatSynthesisInProgress, + group: 'navigation', + order: -1 + }, + { + id: TerminalChatExecute, + when: ScopedChatSynthesisInProgress, + group: 'navigation', + order: -1 + } + ] + }); + } + + async run(accessor: ServicesAccessor) { + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).stop(); + } +} + +export class StopReadChatItemAloud extends Action2 { + + static readonly ID = 'workbench.action.chat.stopReadChatItemAloud'; + + constructor() { + super({ + id: StopReadChatItemAloud.ID, + icon: Codicon.mute, + title: localize2('workbench.action.chat.stopReadChatItemAloud', "Stop Reading Aloud"), + precondition: ScopedChatSynthesisInProgress, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape, + }, + menu: [ + { + id: MenuId.ChatMessageTitle, + when: ContextKeyExpr.and( + ScopedChatSynthesisInProgress, // only when in progress + CONTEXT_RESPONSE, // only for responses + CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered + ), + group: 'navigation' + } + ] + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).stop(); + } +} + +//#endregion + +//#region Keyword Recognition + +function supportsKeywordActivation(configurationService: IConfigurationService, speechService: ISpeechService, chatAgentService: IChatAgentService): boolean { + if (!speechService.hasSpeechProvider || !chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { return false; } @@ -882,7 +1010,7 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe @IInstantiationService instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IHostService private readonly hostService: IHostService, - @IChatService private readonly chatService: IChatService + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { super(); @@ -897,7 +1025,14 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe this.handleKeywordActivation(); })); - this._register(this.chatService.onDidRegisterProvider(() => this.updateConfiguration())); + const onDidAddDefaultAgent = this._register(this.chatAgentService.onDidChangeAgents(() => { + if (this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { + this.updateConfiguration(); + this.handleKeywordActivation(); + + onDidAddDefaultAgent.dispose(); + } + })); this._register(this.speechService.onDidStartSpeechToTextSession(() => this.handleKeywordActivation())); this._register(this.speechService.onDidEndSpeechToTextSession(() => this.handleKeywordActivation())); @@ -910,7 +1045,7 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe } private updateConfiguration(): void { - if (!this.speechService.hasSpeechProvider || this.chatService.getProviderInfos().length === 0) { + if (!this.speechService.hasSpeechProvider || !this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { return; // these settings require a speech and chat provider } @@ -944,7 +1079,7 @@ export class KeywordActivationContribution extends Disposable implements IWorkbe private handleKeywordActivation(): void { const enabled = - supportsKeywordActivation(this.configurationService, this.speechService) && + supportsKeywordActivation(this.configurationService, this.speechService, this.chatAgentService) && !this.speechService.hasActiveSpeechToTextSession; if ( (enabled && this.activeSession) || @@ -1030,6 +1165,7 @@ class KeywordActivationStatusEntry extends Disposable { @IStatusbarService private readonly statusbarService: IStatusbarService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -1050,7 +1186,7 @@ class KeywordActivationStatusEntry extends Disposable { } private updateStatusEntry(): void { - const visible = supportsKeywordActivation(this.configurationService, this.speechService); + const visible = supportsKeywordActivation(this.configurationService, this.speechService, this.chatAgentService); if (visible) { if (!this.entry.value) { this.createStatusEntry(); @@ -1082,3 +1218,148 @@ class KeywordActivationStatusEntry extends Disposable { this.entry.value?.update(this.getStatusEntryProperties()); } } + +//#endregion + +//#region Install Provider Actions + +const InstallingSpeechProvider = new RawContextKey('installingSpeechProvider', false, true); + +abstract class BaseInstallSpeechProviderAction extends Action2 { + + private static readonly SPEECH_EXTENSION_ID = 'ms-vscode.vscode-speech'; + + async run(accessor: ServicesAccessor): Promise { + const contextKeyService = accessor.get(IContextKeyService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + try { + InstallingSpeechProvider.bindTo(contextKeyService).set(true); + await extensionsWorkbenchService.install(BaseInstallSpeechProviderAction.SPEECH_EXTENSION_ID, { + justification: this.getJustification(), + enable: true + }, ProgressLocation.Notification); + } finally { + InstallingSpeechProvider.bindTo(contextKeyService).reset(); + } + } + + protected abstract getJustification(): string; +} + +export class InstallSpeechProviderForVoiceChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForVoiceChat'; + + constructor() { + super({ + id: InstallSpeechProviderForVoiceChatAction.ID, + title: localize2('workbench.action.chat.installProviderForVoiceChat.label', "Start Voice Chat"), + icon: Codicon.mic, + precondition: InstallingSpeechProvider.negate(), + menu: [{ + id: MenuId.ChatExecute, + when: HasSpeechProvider.negate(), + group: 'navigation', + order: -1 + }, { + id: TerminalChatExecute, + when: HasSpeechProvider.negate(), + group: 'navigation', + order: -1 + }] + }); + } + + protected getJustification(): string { + return localize('installProviderForVoiceChat.justification', "Microphone support requires this extension."); + } +} + +export class InstallSpeechProviderForSynthesizeChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForSynthesis'; + + constructor() { + super({ + id: InstallSpeechProviderForSynthesizeChatAction.ID, + title: localize2('workbench.action.chat.installProviderForSynthesis.label', "Read Aloud"), + icon: Codicon.unmute, + precondition: InstallingSpeechProvider.negate(), + menu: [{ + id: MenuId.ChatMessageTitle, + when: HasSpeechProvider.negate(), + group: 'navigation' + }] + }); + } + + protected getJustification(): string { + return localize('installProviderForSynthesis.justification', "Speaker support requires this extension."); + } +} + +//#endregion + +registerThemingParticipant((theme, collector) => { + let activeRecordingColor: Color | undefined; + let activeRecordingDimmedColor: Color | undefined; + if (theme.type === ColorScheme.LIGHT || theme.type === ColorScheme.DARK) { + activeRecordingColor = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND) ?? theme.getColor(focusBorder); + activeRecordingDimmedColor = activeRecordingColor?.transparent(0.38); + } else { + activeRecordingColor = theme.getColor(contrastBorder); + activeRecordingDimmedColor = theme.getColor(contrastBorder); + } + + // Show a "microphone" or "pulse" icon when speech-to-text or text-to-speech is in progress that glows via outline. + collector.addRule(` + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + color: ${activeRecordingColor}; + outline: 1px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1s infinite; + border-radius: 50%; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::after, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { + outline: 2px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; + } + + @keyframes pulseAnimation { + 0% { + outline-width: 2px; + } + 62% { + outline-width: 5px; + outline-color: ${activeRecordingDimmedColor}; + } + 100% { + outline-width: 2px; + } + } + `); +}); 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 186a6a715c1..9fca2cd9497 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForSynthesizeChatAction, InstallSpeechProviderForVoiceChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; registerAction2(StartVoiceChatAction); -registerAction2(InstallVoiceChatAction); +registerAction2(InstallSpeechProviderForVoiceChatAction); registerAction2(VoiceChatInChatViewAction); registerAction2(HoldToVoiceChatInChatViewAction); @@ -18,9 +18,9 @@ registerAction2(InlineVoiceChatAction); registerAction2(StopListeningAction); registerAction2(StopListeningAndSubmitAction); -registerAction2(StopListeningInChatViewAction); -registerAction2(StopListeningInChatEditorAction); -registerAction2(StopListeningInQuickChatAction); -registerAction2(StopListeningInTerminalChatAction); +registerAction2(ReadChatResponseAloud); +registerAction2(StopReadChatItemAloud); +registerAction2(StopReadAloud); +registerAction2(InstallSpeechProviderForSynthesizeChatAction); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap new file mode 100644 index 00000000000..67f63f14b70 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap @@ -0,0 +1 @@ +
<!--[CDATA[<div-->content]]>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap new file mode 100644 index 00000000000..9def37d5acb --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap @@ -0,0 +1 @@ +
<!-- comment1 <div></div> -->
content
<!-- comment2 -->
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap new file mode 100644 index 00000000000..9bfd3b945e8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap @@ -0,0 +1 @@ +

1<canvas>2</canvas>

<details>3</details>4

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap new file mode 100644 index 00000000000..c0b5a277aac --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap @@ -0,0 +1 @@ +

1

<details id="id1" style="display: none">2<details id="my id 2">3</details></details>4

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap new file mode 100644 index 00000000000..ba72307e533 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap @@ -0,0 +1,8 @@ +

heading

+<details> +
    +
  • <details>1</details>
  • +
  • hi
  • +
+</details> +
<canvas>canvas here</canvas>
<details></details>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap new file mode 100644 index 00000000000..1241ef62b5f --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap @@ -0,0 +1 @@ +

<img src="http://disallowed.com/image.jpg">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap new file mode 100644 index 00000000000..5b482726d3a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap @@ -0,0 +1 @@ +

<area>



<input type="text" value="test">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap new file mode 100644 index 00000000000..2e65efe2a14 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap new file mode 100644 index 00000000000..89991e7676e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap @@ -0,0 +1 @@ +

hello

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap new file mode 100644 index 00000000000..b9ca8267b0c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap @@ -0,0 +1,4 @@ +
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap new file mode 100644 index 00000000000..df6a95f4b5d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap @@ -0,0 +1,6 @@ +

heading

+
    +
  • 1
  • +
  • hi
  • +
+
code here
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts new file mode 100644 index 00000000000..657ed1c961c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { assertSnapshot } from 'vs/base/test/common/snapshot'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; +import { MockTrustedDomainService } from 'vs/workbench/contrib/url/test/browser/mockTrustedDomainService'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('ChatMarkdownRenderer', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let testRenderer: ChatMarkdownRenderer; + setup(() => { + const instantiationService = store.add(workbenchInstantiationService(undefined, store)); + instantiationService.stub(ITrustedDomainService, new MockTrustedDomainService(['http://allowed.com'])); + testRenderer = instantiationService.createInstance(ChatMarkdownRenderer, {}); + }); + + test('simple', async () => { + const md = new MarkdownString('a'); + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.textContent); + }); + + test('supportHtml with one-line markdown', async () => { + const md = new MarkdownString('**hello**'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + + const md2 = new MarkdownString('1. [_hello_](https://example.com) test **text**'); + md2.supportHtml = true; + const result2 = store.add(testRenderer.render(md2)); + await assertSnapshot(result2.element.outerHTML); + }); + + test('invalid HTML', async () => { + const md = new MarkdownString('12
3
4'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('invalid HTML with attributes', async () => { + const md = new MarkdownString('14'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('valid HTML', async () => { + const md = new MarkdownString(` +

heading

+
    +
  • 1
  • +
  • hi
  • +
+
code here
`); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('mixed valid and invalid HTML', async () => { + const md = new MarkdownString(` +

heading

+
+
    +
  • 1
  • +
  • hi
  • +
+
+
canvas here
`); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('self-closing elements', async () => { + const md = new MarkdownString('

'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('html comments', async () => { + const md = new MarkdownString('
content
'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('CDATA', async () => { + const md = new MarkdownString('content]]>'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('remote images', async () => { + const md = new MarkdownString(' '); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts index 6d6856156ee..2da25bc9817 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; @@ -17,6 +19,7 @@ import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVari import { MockChatWidgetService } from 'vs/workbench/contrib/chat/test/browser/mockChatWidget'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { TestViewsService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ChatVariables', function () { @@ -26,26 +29,27 @@ suite('ChatVariables', function () { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(function () { - service = new ChatVariablesService(new MockChatWidgetService()); + service = new ChatVariablesService(new MockChatWidgetService(), new TestViewsService()); instantiationService = testDisposables.add(new TestInstantiationService()); instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatVariablesService, service); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); }); test('ChatVariables - resolveVariables', async function () { - const v1 = service.registerVariable({ name: 'foo', description: 'bar' }, async () => ([{ level: 'full', value: 'farboo' }])); - const v2 = service.registerVariable({ name: 'far', description: 'boo' }, async () => ([{ level: 'full', value: 'farboo' }])); + const v1 = service.registerVariable({ id: 'id', name: 'foo', description: 'bar' }, async () => 'farboo'); + const v2 = service.registerVariable({ id: 'id', name: 'far', description: 'boo' }, async () => 'farboo'); const parser = instantiationService.createInstance(ChatRequestParser); const resolveVariables = async (text: string) => { const result = parser.parseChatRequest('1', text); - return await service.resolveVariables(result, null!, () => { }, CancellationToken.None); + return await service.resolveVariables(result, undefined, null!, () => { }, CancellationToken.None); }; { diff --git a/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts b/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts index 7e23620a06e..472877f3e4b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts +++ b/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts @@ -14,13 +14,6 @@ export class MockChatWidgetService implements IChatWidgetService { */ readonly lastFocusedWidget: IChatWidget | undefined; - /** - * Returns whether a view was successfully revealed. - */ - async revealViewForProvider(providerId: string): Promise { - return undefined; - } - getWidgetByInputUri(uri: URI): IChatWidget | undefined { return undefined; } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap index 11c9c2ef292..730cb160f96 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap @@ -4,7 +4,8 @@ value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newline", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap index bc1d5cda51e..714c3f06ed6 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap @@ -4,7 +4,8 @@ value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newlinecontent with vuln\nand\nnewlines", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap index 229ab7c6ac4..f68fd93b8bd 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap @@ -4,7 +4,8 @@ value: "some code content with vuln after", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index 913d93883a3..bffaba0f3ea 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -32,8 +32,11 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, + publisherDisplayName: "", + extensionDisplayName: "", + extensionPublisherId: "", locations: [ "panel" ], - metadata: { description: "" }, + metadata: { }, slashCommands: [ { name: "subCommand", diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index 847804842be..b2392476b7c 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -32,8 +32,11 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, + publisherDisplayName: "", + extensionDisplayName: "", + extensionPublisherId: "", locations: [ "panel" ], - metadata: { description: "" }, + metadata: { }, slashCommands: [ { name: "subCommand", diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 42a9e4e71d0..1965499f050 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -18,8 +18,11 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, + publisherDisplayName: "", + extensionDisplayName: "", + extensionPublisherId: "", locations: [ "panel" ], - metadata: { description: "" }, + metadata: { }, slashCommands: [ { name: "subCommand", diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index 50301b0cf1c..4ec681f874b 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -18,8 +18,11 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, + publisherDisplayName: "", + extensionDisplayName: "", + extensionPublisherId: "", locations: [ "panel" ], - metadata: { description: "" }, + metadata: { }, slashCommands: [ { name: "subCommand", diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index 8de370325aa..fde8b6955f1 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -18,8 +18,11 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, + publisherDisplayName: "", + extensionDisplayName: "", + extensionPublisherId: "", locations: [ "panel" ], - metadata: { description: "" }, + metadata: { }, slashCommands: [ { name: "subCommand", diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 855c14d6033..0a210c51cfc 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -18,8 +18,11 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, + publisherDisplayName: "", + extensionDisplayName: "", + extensionPublisherId: "", locations: [ "panel" ], - metadata: { description: "" }, + metadata: { }, slashCommands: [ { name: "subCommand", @@ -87,6 +90,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { @@ -116,6 +120,7 @@ }, variableName: "debugConsole", variableArg: "", + variableId: "copilot.debugConsole", kind: "var" } ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index c33398a6d33..874558504ef 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -18,8 +18,11 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, + publisherDisplayName: "", + extensionDisplayName: "", + extensionPublisherId: "", locations: [ "panel" ], - metadata: { description: "" }, + metadata: { }, slashCommands: [ { name: "subCommand", @@ -56,6 +59,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { @@ -85,6 +89,7 @@ }, variableName: "debugConsole", variableArg: "", + variableId: "copilot.debugConsole", kind: "var" } ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap index 17b89a36298..d826520078a 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap @@ -27,6 +27,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap index 5af3a10d3b8..8334f7ba2b0 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap @@ -27,6 +27,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index b9b4b15aa1f..9b85a97b4d7 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -1,8 +1,9 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, + initialLocation: "panel", welcomeMessage: undefined, requests: [ { @@ -10,7 +11,6 @@ text: "@ChatProviderWithUsedContext test request", parts: [ { - kind: "agent", range: { start: 0, endExclusive: 28 @@ -22,11 +22,20 @@ endColumn: 29 }, agent: { - id: "ChatProviderWithUsedContext", name: "ChatProviderWithUsedContext", - description: undefined, - metadata: { } - } + id: "ChatProviderWithUsedContext", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", + locations: [ "panel" ], + metadata: { }, + slashCommands: [ ] + }, + kind: "agent" }, { range: { @@ -51,31 +60,24 @@ isCanceled: false, vote: undefined, agent: { - id: "ChatProviderWithUsedContext", name: "ChatProviderWithUsedContext", - description: undefined, + id: "ChatProviderWithUsedContext", extensionId: { value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { }, - slashCommands: [ ], + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", locations: [ "panel" ], - isDefault: undefined + metadata: { }, + slashCommands: [ ] }, slashCommand: undefined, usedContext: { documents: [ { - uri: { - scheme: "file", - authority: "", - path: "/test/path/to/file", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, + uri: URI(file:///test/path/to/file), version: 3, ranges: [ { @@ -89,8 +91,8 @@ ], kind: "usedContext" }, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } - ], - providerId: "testProvider" + ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap index 75c5fa71f40..a0e87754398 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap @@ -1,9 +1,9 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, + initialLocation: "panel", welcomeMessage: undefined, - requests: [ ], - providerId: "testProvider" + requests: [ ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index bad39b3eafc..421835ade92 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -1,15 +1,15 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, + initialLocation: "panel", welcomeMessage: undefined, requests: [ { message: { parts: [ { - kind: "agent", range: { start: 0, endExclusive: 28 @@ -21,14 +21,20 @@ endColumn: 29 }, agent: { - id: "ChatProviderWithUsedContext", name: "ChatProviderWithUsedContext", - description: undefined, - metadata: { - requester: { name: "test" }, - fullName: "test" - } - } + id: "ChatProviderWithUsedContext", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", + locations: [ "panel" ], + metadata: { requester: { name: "test" } }, + slashCommands: [ ] + }, + kind: "agent" }, { range: { @@ -54,34 +60,24 @@ isCanceled: false, vote: undefined, agent: { - id: "ChatProviderWithUsedContext", name: "ChatProviderWithUsedContext", - description: undefined, + id: "ChatProviderWithUsedContext", extensionId: { value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { - requester: { name: "test" }, - fullName: "test" - }, - slashCommands: [ ], + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", locations: [ "panel" ], - isDefault: undefined + metadata: { requester: { name: "test" } }, + slashCommands: [ ] }, slashCommand: undefined, usedContext: { documents: [ { - uri: { - scheme: "file", - authority: "", - path: "/test/path/to/file", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, + uri: URI(file:///test/path/to/file), version: 3, ranges: [ { @@ -95,8 +91,8 @@ ], kind: "usedContext" }, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } - ], - providerId: "testProvider" + ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap similarity index 57% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap rename to src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index cfed0a5d0ca..d0b833b4c19 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -1,16 +1,15 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, + initialLocation: "panel", welcomeMessage: undefined, requests: [ { message: { - text: "@ChatProviderWithUsedContext test request", parts: [ { - kind: "agent", range: { start: 0, endExclusive: 28 @@ -22,9 +21,20 @@ endColumn: 29 }, agent: { + name: "ChatProviderWithUsedContext", id: "ChatProviderWithUsedContext", - metadata: { description: undefined } - } + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", + locations: [ "panel" ], + metadata: { }, + slashCommands: [ ] + }, + kind: "agent" }, { range: { @@ -40,53 +50,33 @@ text: " test request", kind: "text" } - ] + ], + text: "@ChatProviderWithUsedContext test request" }, variableData: { variables: [ ] }, response: [ ], - result: { metadata: { metadataKey: "value" } }, + result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, followups: undefined, isCanceled: false, vote: undefined, agent: { + name: "ChatProviderWithUsedContext", id: "ChatProviderWithUsedContext", extensionId: { value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { description: undefined }, - slashCommands: [ ], + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", locations: [ "panel" ], - isDefault: undefined + metadata: { }, + slashCommands: [ ] }, slashCommand: undefined, - usedContext: { - documents: [ - { - uri: { - scheme: "file", - authority: "", - path: "/test/path/to/file", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, - version: 3, - ranges: [ - { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 2, - endColumn: 2 - } - ] - } - ], - kind: "usedContext" - }, - contentReferences: [ ] + usedContext: undefined, + contentReferences: [ ], + codeCitations: [ ] } - ], - providerId: "testProvider" + ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap deleted file mode 100644 index 75c5fa71f40..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap +++ /dev/null @@ -1,9 +0,0 @@ -{ - requesterUsername: "test", - requesterAvatarIconUri: undefined, - responderUsername: "test", - responderAvatarIconUri: undefined, - welcomeMessage: undefined, - requests: [ ], - providerId: "testProvider" -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap deleted file mode 100644 index cb9c94b5d2d..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ /dev/null @@ -1,100 +0,0 @@ -{ - requesterUsername: "test", - requesterAvatarIconUri: undefined, - responderUsername: "test", - responderAvatarIconUri: undefined, - welcomeMessage: undefined, - requests: [ - { - message: { - parts: [ - { - kind: "agent", - range: { - start: 0, - endExclusive: 28 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 29 - }, - agent: { - id: "ChatProviderWithUsedContext", - metadata: { - description: undefined, - requester: { name: "test" }, - fullName: "test" - } - } - }, - { - range: { - start: 28, - endExclusive: 41 - }, - editorRange: { - startLineNumber: 1, - startColumn: 29, - endLineNumber: 1, - endColumn: 42 - }, - text: " test request", - kind: "text" - } - ], - text: "@ChatProviderWithUsedContext test request" - }, - variableData: { variables: [ ] }, - response: [ ], - result: { metadata: { metadataKey: "value" } }, - followups: undefined, - isCanceled: false, - vote: undefined, - agent: { - id: "ChatProviderWithUsedContext", - extensionId: { - value: "nullExtensionDescription", - _lower: "nullextensiondescription" - }, - metadata: { - description: undefined, - requester: { name: "test" }, - fullName: "test" - }, - slashCommands: [ ], - locations: [ "panel" ], - isDefault: undefined - }, - slashCommand: undefined, - usedContext: { - documents: [ - { - uri: { - scheme: "file", - authority: "", - path: "/test/path/to/file", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, - version: 3, - ranges: [ - { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 2, - endColumn: 2 - } - ] - } - ], - kind: "usedContext" - }, - contentReferences: [ ] - } - ], - providerId: "testProvider" -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap index 5847d813dfe..b26d84334a0 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap @@ -9,15 +9,7 @@ kind: "markdownContent" }, { - inlineReference: { - scheme: "https", - authority: "microsoft.com", - path: "/", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, + inlineReference: URI(https://microsoft.com/), kind: "inlineReference" }, { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__markdown.0.snap new file mode 100644 index 00000000000..a7cb1f9bd3a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__markdown.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "markdown1markdown2", + isTrusted: { enabledCommands: [ ] }, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_mergeable_markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_mergeable_markdown.0.snap new file mode 100644 index 00000000000..799444ae923 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_mergeable_markdown.0.snap @@ -0,0 +1,12 @@ +[ + { + content: { + value: "markdown1markdown2", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false, + baseUri: undefined + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_not_mergeable_markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_not_mergeable_markdown.0.snap new file mode 100644 index 00000000000..05c671ef5aa --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_not_mergeable_markdown.0.snap @@ -0,0 +1,20 @@ +[ + { + content: { + value: "markdown1", + isTrusted: false, + supportThemeIcons: false, + supportHtml: true + }, + kind: "markdownContent" + }, + { + content: { + value: "markdown2", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ 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 new file mode 100644 index 00000000000..f14ffdaa651 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MockObject, mockObject } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ChatAgentService, IChatAgentData, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import assert from 'assert'; + +const testAgentId = 'testAgent'; +const testAgentData: IChatAgentData = { + id: testAgentId, + name: 'Test Agent', + extensionDisplayName: '', + extensionId: new ExtensionIdentifier(''), + extensionPublisherId: '', + locations: [], + metadata: {}, + slashCommands: [], +}; + +suite('ChatAgents', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let chatAgentService: IChatAgentService; + let contextKeyService: MockObject; + setup(() => { + contextKeyService = mockObject()(); + chatAgentService = new ChatAgentService(contextKeyService as any); + }); + + test('registerAgent', async () => { + assert.strictEqual(chatAgentService.getAgents().length, 0); + + + const agentRegistration = chatAgentService.registerAgent(testAgentId, testAgentData); + + assert.strictEqual(chatAgentService.getAgents().length, 1); + assert.strictEqual(chatAgentService.getAgents()[0].id, testAgentId); + + assert.throws(() => chatAgentService.registerAgent(testAgentId, testAgentData)); + + agentRegistration.dispose(); + assert.strictEqual(chatAgentService.getAgents().length, 0); + }); + + test('agent when clause', async () => { + assert.strictEqual(chatAgentService.getAgents().length, 0); + + store.add(chatAgentService.registerAgent(testAgentId, { + ...testAgentData, + when: 'myKey' + })); + assert.strictEqual(chatAgentService.getAgents().length, 0); + + contextKeyService.contextMatchesRules.returns(true); + assert.strictEqual(chatAgentService.getAgents().length, 1); + }); + + suite('registerAgentImplementation', function () { + const agentImpl: IChatAgentImplementation = { + invoke: async () => { return {}; }, + provideFollowups: async () => { return []; }, + }; + + test('should register an agent implementation', () => { + store.add(chatAgentService.registerAgent(testAgentId, testAgentData)); + store.add(chatAgentService.registerAgentImplementation(testAgentId, agentImpl)); + + const agents = chatAgentService.getActivatedAgents(); + assert.strictEqual(agents.length, 1); + assert.strictEqual(agents[0].id, testAgentId); + }); + + test('can dispose an agent implementation', () => { + store.add(chatAgentService.registerAgent(testAgentId, testAgentData)); + const implRegistration = chatAgentService.registerAgentImplementation(testAgentId, agentImpl); + implRegistration.dispose(); + + const agents = chatAgentService.getActivatedAgents(); + assert.strictEqual(agents.length, 0); + }); + + test('should throw error if agent does not exist', () => { + assert.throws(() => chatAgentService.registerAgentImplementation('nonexistentAgent', agentImpl)); + }); + + test('should throw error if agent already has an implementation', () => { + store.add(chatAgentService.registerAgent(testAgentId, testAgentData)); + store.add(chatAgentService.registerAgentImplementation(testAgentId, agentImpl)); + + assert.throws(() => chatAgentService.registerAgentImplementation(testAgentId, agentImpl)); + }); + }); +}); 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 3d63e2ed9d3..3c0f0bcc656 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { URI } from 'vs/base/common/uri'; @@ -11,10 +11,12 @@ import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Range } from 'vs/editor/common/core/range'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, Response } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -30,11 +32,12 @@ suite('ChatModel', () => { instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); }); test('Waits for initialization', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); let hasInitialized = false; model.waitForInitialization().then(() => { @@ -45,13 +48,13 @@ suite('ChatModel', () => { assert.strictEqual(hasInitialized, false); model.startInitialize(); - model.initialize({} as any, undefined); + model.initialize(undefined); await timeout(0); assert.strictEqual(hasInitialized, true); }); test('must call startInitialize before initialize', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); let hasInitialized = false; model.waitForInitialization().then(() => { @@ -61,12 +64,12 @@ suite('ChatModel', () => { await timeout(0); assert.strictEqual(hasInitialized, false); - assert.throws(() => model.initialize({} as any, undefined)); + assert.throws(() => model.initialize(undefined)); assert.strictEqual(hasInitialized, false); }); test('deinitialize/reinitialize', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); let hasInitialized = false; model.waitForInitialization().then(() => { @@ -74,7 +77,7 @@ suite('ChatModel', () => { }); model.startInitialize(); - model.initialize({} as any, undefined); + model.initialize(undefined); await timeout(0); assert.strictEqual(hasInitialized, true); @@ -85,65 +88,97 @@ suite('ChatModel', () => { }); model.startInitialize(); - model.initialize({} as any, undefined); + model.initialize(undefined); await timeout(0); assert.strictEqual(hasInitialized2, true); }); test('cannot initialize twice', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); model.startInitialize(); - model.initialize({} as any, undefined); - assert.throws(() => model.initialize({} as any, undefined)); + model.initialize(undefined); + assert.throws(() => model.initialize(undefined)); }); test('Initialization fails when model is disposed', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); model.dispose(); - assert.throws(() => model.initialize({} as any, undefined)); + assert.throws(() => model.initialize(undefined)); }); test('removeRequest', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined)); + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); model.startInitialize(); - model.initialize({} as any, undefined); + model.initialize(undefined); const text = 'hello'; - model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }); + model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); const requests = model.getRequests(); assert.strictEqual(requests.length, 1); model.removeRequest(requests[0].id); assert.strictEqual(model.getRequests().length, 0); }); + + test('adoptRequest', async function () { + const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); + const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); + + model1.startInitialize(); + model1.initialize(undefined); + + model2.startInitialize(); + model2.initialize(undefined); + + const text = 'hello'; + const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + + assert.strictEqual(model1.getRequests().length, 1); + assert.strictEqual(model2.getRequests().length, 0); + assert.ok(request1.session === model1); + assert.ok(request1.response?.session === model1); + + model2.adoptRequest(request1); + + assert.strictEqual(model1.getRequests().length, 0); + assert.strictEqual(model2.getRequests().length, 1); + assert.ok(request1.session === model2); + assert.ok(request1.response?.session === model2); + + model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' }); + + assert.strictEqual(request1.response.response.toString(), 'Hello'); + }); }); suite('Response', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); - test('content, markdown', async () => { - const response = new Response([]); - response.updateContent({ content: 'text', kind: 'content' }); - response.updateContent({ content: new MarkdownString('markdown'), kind: 'markdownContent' }); + test('mergeable markdown', async () => { + const response = store.add(new Response([])); + response.updateContent({ content: new MarkdownString('markdown1'), kind: 'markdownContent' }); + response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); await assertSnapshot(response.value); - assert.strictEqual(response.asString(), 'textmarkdown'); + assert.strictEqual(response.toString(), 'markdown1markdown2'); }); - test('markdown, content', async () => { - const response = new Response([]); - response.updateContent({ content: new MarkdownString('markdown'), kind: 'markdownContent' }); - response.updateContent({ content: 'text', kind: 'content' }); + test('not mergeable markdown', async () => { + const response = store.add(new Response([])); + const md1 = new MarkdownString('markdown1'); + md1.supportHtml = true; + response.updateContent({ content: md1, kind: 'markdownContent' }); + response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); await assertSnapshot(response.value); }); test('inline reference', async () => { - const response = new Response([]); - response.updateContent({ content: 'text before', kind: 'content' }); + const response = store.add(new Response([])); + response.updateContent({ content: new MarkdownString('text before'), kind: 'markdownContent' }); response.updateContent({ inlineReference: URI.parse('https://microsoft.com'), kind: 'inlineReference' }); - response.updateContent({ content: 'text after', kind: 'content' }); + response.updateContent({ content: new MarkdownString('text after'), kind: 'markdownContent' }); await assertSnapshot(response.value); }); }); 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 c28817bb090..c0b7a9542a4 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -6,7 +6,9 @@ import { MockObject, mockObject } from 'vs/base/test/common/mock'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ChatAgentLocation, ChatAgentService, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; @@ -31,6 +33,7 @@ suite('ChatRequestParser', () => { instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); varService = mockObject()({}); @@ -86,6 +89,7 @@ suite('ChatRequestParser', () => { test('variables', async () => { varService.hasVariable.returns(true); + varService.getVariable.returns({ id: 'copilot.selection' }); parser = instantiationService.createInstance(ChatRequestParser); const text = 'What does #selection mean?'; @@ -95,6 +99,7 @@ suite('ChatRequestParser', () => { test('variable with question mark', async () => { varService.hasVariable.returns(true); + varService.getVariable.returns({ id: 'copilot.selection' }); parser = instantiationService.createInstance(ChatRequestParser); const text = 'What is #selection?'; @@ -112,7 +117,7 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return { id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: { description: '' }, slashCommands }; + return { id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, publisherDisplayName: '', extensionDisplayName: '', extensionPublisherId: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands } satisfies IChatAgentData; }; test('agent with subcommand after text', async () => { @@ -181,6 +186,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); + varService.getVariable.onCall(0).returns({ id: 'copilot.selection' }); + varService.getVariable.onCall(1).returns({ id: 'copilot.debugConsole' }); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole'); @@ -193,6 +200,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); + varService.getVariable.onCall(0).returns({ id: 'copilot.selection' }); + varService.getVariable.onCall(1).returns({ id: 'copilot.debugConsole' }); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole'); 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 3125cf09d88..da76add7eca 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; -import { ProviderResult } from 'vs/editor/common/languages'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -21,43 +21,27 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ChatAgentLocation, ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; +import { NullWorkbenchAssignmentService } from 'vs/workbench/services/assignment/test/common/nullAssignmentService'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -class SimpleTestProvider extends Disposable implements IChatProvider { - private static sessionId = 0; - - readonly displayName = 'Test'; - - constructor(readonly id: string) { - super(); - } - - async prepareSession(): Promise { - return { - id: SimpleTestProvider.sessionId++, - }; - } - - async provideReply(request: IChatRequest, progress: (progress: IChatProgress) => void): Promise<{ session: IChat; followups: never[] }> { - return { session: request.session, followups: [] }; - } -} - const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContext: IChatAgent = { id: chatAgentWithUsedContextId, name: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, + publisherDisplayName: '', + extensionPublisherId: '', + extensionDisplayName: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [], @@ -93,6 +77,7 @@ suite('ChatService', () => { setup(async () => { instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection( [IChatVariablesService, new MockChatVariablesService()], + [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()] ))); instantiationService.stub(IStorageService, storageService = testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); @@ -100,9 +85,9 @@ suite('ChatService', () => { instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IViewsService, new TestExtensionService()); - instantiationService.stub(IChatContributionService, new TestExtensionService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IChatService, new MockChatService()); chatAgentService = instantiationService.createInstance(ChatAgentService); @@ -113,31 +98,24 @@ suite('ChatService', () => { return {}; }, } satisfies IChatAgentImplementation; - testDisposables.add(chatAgentService.registerAgent('testAgent', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); + testDisposables.add(chatAgentService.registerAgent('testAgent', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, extensionPublisherId: '', publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, extensionPublisherId: '', publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent)); - chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' } }); }); test('retrieveSession', async () => { const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - const provider1 = testDisposables.add(new SimpleTestProvider('provider1')); - const provider2 = testDisposables.add(new SimpleTestProvider('provider2')); - testDisposables.add(testService.registerProvider(provider1)); - testDisposables.add(testService.registerProvider(provider2)); - - const session1 = testDisposables.add(testService.startSession('provider1', CancellationToken.None)); + const session1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); await session1.waitForInitialization(); - session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }); + session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }, 0); - const session2 = testDisposables.add(testService.startSession('provider2', CancellationToken.None)); + const session2 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); await session2.waitForInitialization(); - session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }); + session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }, 0); storageService.flush(); const testService2 = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService2.registerProvider(provider1)); - testDisposables.add(testService2.registerProvider(provider2)); const retrieved1 = testDisposables.add(testService2.getOrRestoreSession(session1.sessionId)!); await retrieved1.waitForInitialization(); const retrieved2 = testDisposables.add(testService2.getOrRestoreSession(session2.sessionId)!); @@ -146,72 +124,35 @@ suite('ChatService', () => { assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2'); }); - test('Handles failed session startup', async () => { - function getFailProvider(providerId: string) { - return new class implements IChatProvider { - readonly id = providerId; - readonly displayName = 'Test'; - - lastInitialState = undefined; - - prepareSession(initialState: any): ProviderResult { - throw new Error('Failed to start session'); - } - - async provideReply(request: IChatRequest) { - return { session: request.session, followups: [] }; - } - }; - } - - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - const provider1 = getFailProvider('provider1'); - testDisposables.add(testService.registerProvider(provider1)); - - const session1 = testDisposables.add(testService.startSession('provider1', CancellationToken.None)); - await assert.rejects(() => session1.waitForInitialization()); - }); - - test('Can\'t register same provider id twice', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - const id = 'testProvider'; - testDisposables.add(testService.registerProvider({ - id, - prepareSession: function (token: CancellationToken): ProviderResult { - throw new Error('Function not implemented.'); - } - })); - - assert.throws(() => { - testDisposables.add(testService.registerProvider({ - id, - prepareSession: function (token: CancellationToken): ProviderResult { - throw new Error('Function not implemented.'); - } - })); - }, 'Expected to throw for dupe provider'); - }); - test('addCompleteRequest', async () => { const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); - const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None)); + const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); assert.strictEqual(model.getRequests().length, 0); - await testService.addCompleteRequest(model.sessionId, 'test request', undefined, { message: 'test response' }); + await testService.addCompleteRequest(model.sessionId, 'test request', undefined, 0, { message: 'test response' }); assert.strictEqual(model.getRequests().length, 1); assert.ok(model.getRequests()[0].response); - assert.strictEqual(model.getRequests()[0].response?.response.asString(), 'test response'); + assert.strictEqual(model.getRequests()[0].response?.response.toString(), 'test response'); + }); + + test('sendRequest fails', async () => { + const testService = testDisposables.add(instantiationService.createInstance(ChatService)); + + const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); + const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`); + assert(response); + await response.responseCompletePromise; + + await assertSnapshot(model.toExport()); }); test('can serialize', async () => { testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); - chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); + chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' } }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); - const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None)); + const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); assert.strictEqual(model.getRequests().length, 0); await assertSnapshot(model.toExport()); @@ -232,9 +173,8 @@ suite('ChatService', () => { // create the first service, send request, get response, and serialize the state { // serapate block to not leak variables in outer scope const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); - const chatModel1 = testDisposables.add(testService.startSession('testProvider', CancellationToken.None)); + const chatModel1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); assert.strictEqual(chatModel1.getRequests().length, 0); const response = await testService.sendRequest(chatModel1.sessionId, `@${chatAgentWithUsedContextId} test request`); @@ -242,13 +182,12 @@ suite('ChatService', () => { await response.responseCompletePromise; - serializedChatData = chatModel1.toJSON(); + serializedChatData = JSON.parse(JSON.stringify(chatModel1)); } // try deserializing the state into a new service const testService2 = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService2.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); const chatModel2 = testService2.loadSessionFromContent(serializedChatData); assert(chatModel2); diff --git a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts index 9441aacda6b..a013abded58 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -13,20 +13,60 @@ suite('ChatWordCounter', () => { function doTest(str: string, nWords: number, resultStr: string) { const result = getNWords(str, nWords); assert.strictEqual(result.value, resultStr); - assert.strictEqual(result.actualWordCount, nWords); + assert.strictEqual(result.returnedWordCount, nWords); } - test('getNWords, matching actualWordCount', () => { - const cases: [string, number, string][] = [ - ['hello world', 1, 'hello'], - ['hello', 1, 'hello'], - ['hello world', 0, ''], - ['here\'s, some. punctuation?', 3, 'here\'s, some. punctuation?'], - ['| markdown | _table_ | header |', 3, '| markdown | _table_ | header'], - ['| --- | --- | --- |', 1, '| --- | --- | --- |'], - [' \t some \n whitespace \n\n\nhere ', 3, ' \t some \n whitespace \n\n\nhere'], - ]; + suite('getNWords', () => { + test('matching actualWordCount', () => { + const cases: [string, number, string][] = [ + ['hello world', 1, 'hello'], + ['hello', 1, 'hello'], + ['hello world', 0, ''], + ['here\'s, some. punctuation?', 3, 'here\'s, some. punctuation?'], + ['| markdown | _table_ | header |', 3, '| markdown | _table_ | header'], + ['| --- | --- | --- |', 1, '| ---'], + ['| --- | --- | --- |', 3, '| --- | --- | ---'], + [' \t some \n whitespace \n\n\nhere ', 3, ' \t some \n whitespace \n\n\nhere'], + ]; - cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); + + test('matching links', () => { + const cases: [string, number, string][] = [ + ['[hello](https://example.com) world', 1, '[hello](https://example.com)'], + ['[hello](https://example.com) world', 2, '[hello](https://example.com) world'], + ['oh [hello](https://example.com "title") world', 1, 'oh'], + ['oh [hello](https://example.com "title") world', 2, 'oh [hello](https://example.com "title")'], + // Parens in link destination + ['[hello](https://example.com?()) world', 1, '[hello](https://example.com?())'], + // Escaped brackets in link text + ['[he \\[l\\] \\]lo](https://example.com?()) world', 1, '[he \\[l\\] \\]lo](https://example.com?())'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); + + test('code', () => { + const cases: [string, number, string][] = [ + ['let a=1-2', 2, 'let a'], + ['let a=1-2', 3, 'let a='], + ['let a=1-2', 4, 'let a=1'], + ['const myVar = 1+2', 4, 'const myVar = 1'], + ['
', 3, '
', 4, '
'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); + + test('chinese characters', () => { + const cases: [string, number, string][] = [ + ['我喜欢中国菜', 3, '我喜欢'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); }); + }); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts new file mode 100644 index 00000000000..ebdde0c0d22 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AsyncIterableSource, DeferredPromise, timeout } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { ChatMessageRole, IChatResponseFragment, languageModelExtensionPoint, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +suite('LanguageModels', function () { + + let languageModels: LanguageModelsService; + + const store = new DisposableStore(); + const activationEvents = new Set(); + + setup(function () { + + languageModels = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + activationEvents.add(name); + return Promise.resolve(); + } + }, + new NullLogService() + ); + + const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelExtensionPoint.name)!; + + ext.acceptUsers([{ + description: { ...nullExtensionDescription, enabledApiProposals: ['chatProvider'] }, + value: { vendor: 'test-vendor' }, + collector: null! + }]); + + + store.add(languageModels.registerLanguageModelChat('1', { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Pretty Name', + vendor: 'test-vendor', + family: 'test-family', + version: 'test-version', + id: 'test-id', + maxInputTokens: 100, + maxOutputTokens: 100, + }, + sendChatRequest: async () => { + throw new Error(); + }, + provideTokenCount: async () => { + throw new Error(); + } + })); + + store.add(languageModels.registerLanguageModelChat('12', { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Pretty Name', + vendor: 'test-vendor', + family: 'test2-family', + version: 'test2-version', + id: 'test-id', + maxInputTokens: 100, + maxOutputTokens: 100, + }, + sendChatRequest: async () => { + throw new Error(); + }, + provideTokenCount: async () => { + throw new Error(); + } + })); + }); + + teardown(function () { + languageModels.dispose(); + activationEvents.clear(); + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('empty selector returns all', async function () { + + const result1 = await languageModels.selectLanguageModels({}); + assert.deepStrictEqual(result1.length, 2); + assert.deepStrictEqual(result1[0], '1'); + assert.deepStrictEqual(result1[1], '12'); + }); + + test('no warning that a matching model was not found #213716', async function () { + const result1 = await languageModels.selectLanguageModels({ vendor: 'test-vendor' }); + assert.deepStrictEqual(result1.length, 2); + + const result2 = await languageModels.selectLanguageModels({ vendor: 'test-vendor', family: 'FAKE' }); + assert.deepStrictEqual(result2.length, 0); + }); + + test('sendChatRequest returns a response-stream', async function () { + + store.add(languageModels.registerLanguageModelChat('actual', { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Pretty Name', + vendor: 'test-vendor', + family: 'actual-family', + version: 'actual-version', + id: 'actual-lm', + maxInputTokens: 100, + maxOutputTokens: 100, + }, + sendChatRequest: async (messages, _from, _options, token) => { + // const message = messages.at(-1); + + const defer = new DeferredPromise(); + const stream = new AsyncIterableSource(); + + (async () => { + while (!token.isCancellationRequested) { + stream.emitOne({ index: 0, part: { type: 'text', value: Date.now().toString() } }); + await timeout(10); + } + defer.complete(undefined); + })(); + + return { + stream: stream.asyncIterable, + result: defer.p + }; + }, + provideTokenCount: async () => { + throw new Error(); + } + })); + + const models = await languageModels.selectLanguageModels({ identifier: 'actual-lm' }); + assert.ok(models.length === 1); + + const first = models[0]; + + const cts = new CancellationTokenSource(); + + const request = await languageModels.sendChatRequest(first, nullExtensionDescription.identifier, [{ role: ChatMessageRole.User, content: { type: 'text', value: 'hello' } }], {}, cts.token); + + assert.ok(request); + + cts.dispose(true); + + await request.result; + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts deleted file mode 100644 index 27a687cb24d..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts +++ /dev/null @@ -1,31 +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 { IChatContributionService, IChatParticipantContribution, IChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; - -export class MockChatContributionService implements IChatContributionService { - _serviceBrand: undefined; - - constructor( - ) { } - - registeredProviders: IChatProviderContribution[] = []; - registerChatParticipant(participant: IChatParticipantContribution): void { - throw new Error('Method not implemented.'); - } - deregisterChatParticipant(participant: IChatParticipantContribution): void { - throw new Error('Method not implemented.'); - } - - registerChatProvider(provider: IChatProviderContribution): void { - throw new Error('Method not implemented.'); - } - deregisterChatProvider(providerId: string): void { - throw new Error('Method not implemented.'); - } - getViewIdForProvider(providerId: string): string { - throw new Error('Method not implemented.'); - } -} diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index d961bbb74c5..c7cc9199757 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -5,49 +5,50 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCompleteResponse, IChatDetail, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; transferredSessionData: IChatTransferredSessionData | undefined; - hasSessions(providerId: string): boolean { + isEnabled(location: ChatAgentLocation): boolean { + throw new Error('Method not implemented.'); + } + hasSessions(): boolean { throw new Error('Method not implemented.'); } getProviderInfos(): IChatProviderInfo[] { throw new Error('Method not implemented.'); } - startSession(providerId: string, token: CancellationToken): ChatModel | undefined { + startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel | undefined { throw new Error('Method not implemented.'); } getSession(sessionId: string): IChatModel | undefined { + // eslint-disable-next-line local/code-no-dangerous-type-assertions return {} as IChatModel; } - getSessionId(sessionProviderId: number): string | undefined { - throw new Error('Method not implemented.'); - } getOrRestoreSession(sessionId: string): IChatModel | undefined { throw new Error('Method not implemented.'); } loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined { throw new Error('Method not implemented.'); } - onDidRegisterProvider: Event<{ providerId: string }> = undefined!; - onDidUnregisterProvider: Event<{ providerId: string }> = undefined!; - registerProvider(provider: IChatProvider): IDisposable { - throw new Error('Method not implemented.'); - } - /** * Returns whether the request was accepted. */ sendRequest(sessionId: string, message: string): Promise { throw new Error('Method not implemented.'); } + resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions | undefined): Promise { + throw new Error('Method not implemented.'); + } + adoptRequest(sessionId: string, request: IChatRequestModel): Promise { + throw new Error('Method not implemented.'); + } removeRequest(sessionid: string, requestId: string): Promise { throw new Error('Method not implemented.'); } @@ -57,7 +58,7 @@ export class MockChatService implements IChatService { clearSession(sessionId: string): void { throw new Error('Method not implemented.'); } - addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void { + addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void { throw new Error('Method not implemented.'); } getHistory(): IChatDetail[] { @@ -74,7 +75,7 @@ export class MockChatService implements IChatService { notifyUserAction(event: IChatUserActionEvent): void { throw new Error('Method not implemented.'); } - onDidDisposeSession: Event<{ sessionId: string; providerId: string; reason: 'initializationFailed' | 'cleared' }> = undefined!; + onDidDisposeSession: Event<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }> = undefined!; transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { 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 0df8e2b57e7..be7a61b525e 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -5,7 +5,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -31,13 +32,17 @@ export class MockChatVariablesService implements IChatVariablesService { return []; } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { return { variables: [] }; } - resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + attachContext(name: string, value: unknown, location: ChatAgentLocation): void { + throw new Error('Method not implemented.'); + } + + resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } } diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts similarity index 87% rename from src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts rename to src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 97e11f717ea..a1257cad616 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -11,11 +11,12 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; -import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; -import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; +import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; suite('VoiceChat', () => { @@ -27,13 +28,16 @@ suite('VoiceChat', () => { class TestChatAgent implements IChatAgent { extensionId: ExtensionIdentifier = nullExtensionDescription.identifier; + extensionPublisher = ''; + extensionDisplayName = ''; + extensionPublisherId = ''; locations: ChatAgentLocation[] = [ChatAgentLocation.Panel]; public readonly name: string; constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { this.name = id; } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } + provideWelcomeMessage?(location: ChatAgentLocation, token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } metadata = {}; } @@ -48,6 +52,12 @@ suite('VoiceChat', () => { ]; class TestChatAgentService implements IChatAgentService { + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable { + throw new Error('Method not implemented.'); + } + detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { + throw new Error('Method not implemented.'); + } _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } @@ -57,11 +67,16 @@ suite('VoiceChat', () => { getActivatedAgents(): IChatAgent[] { return agents; } getAgents(): IChatAgent[] { return agents; } getDefaultAgent(): IChatAgent | undefined { throw new Error(); } + getContributedDefaultAgent(): IChatAgentData | undefined { throw new Error(); } getSecondaryAgent(): IChatAgent | undefined { throw new Error(); } registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); } getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); } + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable { throw new Error('Method not implemented.'); } + getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); } } class TestSpeechService implements ISpeechService { @@ -71,6 +86,7 @@ suite('VoiceChat', () => { readonly hasSpeechProvider = true; readonly hasActiveSpeechToTextSession = false; + readonly hasActiveTextToSpeechSession = false; readonly hasActiveKeywordRecognition = false; registerSpeechProvider(identifier: string, provider: ISpeechProvider): IDisposable { throw new Error('Method not implemented.'); } @@ -83,6 +99,16 @@ suite('VoiceChat', () => { }; } + onDidStartTextToSpeechSession = Event.None; + onDidEndTextToSpeechSession = Event.None; + + async createTextToSpeechSession(token: CancellationToken): Promise { + return { + onDidChange: Event.None, + synthesize: async () => { } + }; + } + onDidStartKeywordRecognition = Event.None; onDidEndKeywordRecognition = Event.None; recognizeKeyword(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } @@ -105,7 +131,7 @@ suite('VoiceChat', () => { setup(() => { emitter = disposables.add(new Emitter()); - service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService())); + service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService(), new MockContextKeyService())); }); teardown(() => { diff --git a/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts new file mode 100644 index 00000000000..85d4a9cdb2f --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/test/common/utils'; +import { parseNextChatResponseChunk } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; + +suite('VoiceChatActions', function () { + + function assertChunk(text: string, expected: string | undefined, offset: number): { chunk: string | undefined; offset: number } { + const res = parseNextChatResponseChunk(text, offset); + assert.strictEqual(res.chunk, expected); + + return res; + } + + test('parseNextChatResponseChunk', function () { + + // Simple, no offset + assertChunk('Hello World', undefined, 0); + assertChunk('Hello World.', undefined, 0); + assertChunk('Hello World. ', 'Hello World.', 0); + assertChunk('Hello World? ', 'Hello World?', 0); + assertChunk('Hello World! ', 'Hello World!', 0); + assertChunk('Hello World: ', 'Hello World:', 0); + + // Ensure chunks are parsed from the end, no offset + assertChunk('Hello World. How is your day? And more...', 'Hello World. How is your day?', 0); + + // Ensure chunks are parsed from the end, with offset + let offset = assertChunk('Hello World. How is your ', 'Hello World.', 0).offset; + offset = assertChunk('Hello World. How is your day? And more...', 'How is your day?', offset).offset; + offset = assertChunk('Hello World. How is your day? And more to come! ', 'And more to come!', offset).offset; + assertChunk('Hello World. How is your day? And more to come! ', undefined, offset); + + // Sparted by newlines + offset = assertChunk('Hello World.\nHow is your', 'Hello World.', 0).offset; + assertChunk('Hello World.\nHow is your day?\n', 'How is your day?', offset); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts index afcf9915643..b8158d25544 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActions.contribution.ts @@ -11,7 +11,7 @@ import { CodeActionsExtensionPoint, codeActionsExtensionPointDescriptor } from ' import { DocumentationExtensionPoint, documentationExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/documentationExtensionPoint'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { CodeActionsContribution, editorConfiguration } from './codeActionsContribution'; +import { CodeActionsContribution, editorConfiguration, notebookEditorConfiguration } from './codeActionsContribution'; import { CodeActionDocumentationContribution } from './documentationContribution'; const codeActionsExtensionPoint = ExtensionsRegistry.registerExtensionPoint(codeActionsExtensionPointDescriptor); @@ -20,6 +20,9 @@ const documentationExtensionPoint = ExtensionsRegistry.registerExtensionPoint(Extensions.Configuration) .registerConfiguration(editorConfiguration); +Registry.as(Extensions.Configuration) + .registerConfiguration(notebookEditorConfiguration); + class WorkbenchConfigurationContribution { constructor( @IInstantiationService instantiationService: IInstantiationService, diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index 38fd5502ae2..3f075cb5eeb 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts @@ -8,6 +8,7 @@ import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { codeActionCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; @@ -34,15 +35,26 @@ const createCodeActionsAutoSave = (description: string): IJSONSchema => { }; }; -const codeActionsOnSaveDefaultProperties = Object.freeze({ - 'source.fixAll': createCodeActionsAutoSave(nls.localize('codeActionsOnSave.fixAll', "Controls whether auto fix action should be run on file save.")), -}); +const createNotebookCodeActionsAutoSave = (description: string): IJSONSchema => { + return { + type: ['string', 'boolean'], + enum: ['explicit', 'never', true, false], + enumDescriptions: [ + nls.localize('explicit', 'Triggers Code Actions only when explicitly saved.'), + nls.localize('never', 'Never triggers Code Actions on save.'), + nls.localize('explicitBoolean', 'Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of "explicit".'), + nls.localize('neverBoolean', 'Triggers Code Actions only when explicitly saved. This value will be deprecated in favor of "never".') + ], + default: 'explicit', + description: description + }; +}; + const codeActionsOnSaveSchema: IConfigurationPropertySchema = { oneOf: [ { type: 'object', - properties: codeActionsOnSaveDefaultProperties, additionalProperties: { type: 'string' }, @@ -69,18 +81,57 @@ export const editorConfiguration = Object.freeze({ } }); +const notebookCodeActionsOnSaveSchema: IConfigurationPropertySchema = { + oneOf: [ + { + type: 'object', + additionalProperties: { + type: 'string' + }, + }, + { + type: 'array', + items: { type: 'string' } + } + ], + markdownDescription: nls.localize('notebook.codeActionsOnSave', 'Run a series of Code Actions for a notebook on save. Code Actions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `"notebook.source.organizeImports": "explicit"`'), + type: 'object', + additionalProperties: { + type: ['string', 'boolean'], + enum: ['explicit', 'never', true, false], + // enum: ['explicit', 'always', 'never'], -- autosave support needs to be built first + // nls.localize('always', 'Always triggers Code Actions on save, including autosave, focus, and window change events.'), + }, + default: {} +}; + +export const notebookEditorConfiguration = Object.freeze({ + ...editorConfigurationBaseNode, + properties: { + 'notebook.codeActionsOnSave': notebookCodeActionsOnSaveSchema + } +}); + export class CodeActionsContribution extends Disposable implements IWorkbenchContribution { private _contributedCodeActions: CodeActionsExtensionPoint[] = []; + private settings: Set = new Set(); private readonly _onDidChangeContributions = this._register(new Emitter()); constructor( codeActionsExtensionPoint: IExtensionPoint, @IKeybindingService keybindingService: IKeybindingService, + @ILanguageFeaturesService private readonly languageFeatures: ILanguageFeaturesService ) { super(); + // TODO: @justschen caching of code actions based on extensions loaded: https://github.com/microsoft/vscode/issues/216019 + languageFeatures.codeActionProvider.onDidChange(() => { + this.updateSettingsFromCodeActionProviders(); + this.updateConfigurationSchemaFromContribs(); + }, 2000); + codeActionsExtensionPoint.setHandler(extensionPoints => { this._contributedCodeActions = extensionPoints.flatMap(x => x.value).filter(x => Array.isArray(x.actions)); this.updateConfigurationSchema(this._contributedCodeActions); @@ -93,26 +144,54 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon }); } + private updateSettingsFromCodeActionProviders(): void { + const providers = this.languageFeatures.codeActionProvider.allNoModel(); + providers.forEach(provider => { + if (provider.providedCodeActionKinds) { + provider.providedCodeActionKinds.forEach(kind => { + if (!this.settings.has(kind) && CodeActionKind.Source.contains(new HierarchicalKind(kind))) { + this.settings.add(kind); + } + }); + } + }); + } + private updateConfigurationSchema(codeActionContributions: readonly CodeActionsExtensionPoint[]) { - const newProperties: IJSONSchemaMap = { ...codeActionsOnSaveDefaultProperties }; + const newProperties: IJSONSchemaMap = {}; + const newNotebookProperties: IJSONSchemaMap = {}; for (const [sourceAction, props] of this.getSourceActions(codeActionContributions)) { + this.settings.add(sourceAction); newProperties[sourceAction] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title)); + newNotebookProperties[sourceAction] = createNotebookCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title)); } codeActionsOnSaveSchema.properties = newProperties; + notebookCodeActionsOnSaveSchema.properties = newNotebookProperties; + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(editorConfiguration); + } + + private updateConfigurationSchemaFromContribs() { + const properties: IJSONSchemaMap = { ...codeActionsOnSaveSchema.properties }; + const notebookProperties: IJSONSchemaMap = { ...notebookCodeActionsOnSaveSchema.properties }; + for (const codeActionKind of this.settings) { + if (!properties[codeActionKind]) { + properties[codeActionKind] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", codeActionKind)); + notebookProperties[codeActionKind] = createNotebookCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", codeActionKind)); + } + } + codeActionsOnSaveSchema.properties = properties; + notebookCodeActionsOnSaveSchema.properties = notebookProperties; Registry.as(Extensions.Configuration) .notifyConfigurationSchemaUpdated(editorConfiguration); } private getSourceActions(contributions: readonly CodeActionsExtensionPoint[]) { - const defaultKinds = Object.keys(codeActionsOnSaveDefaultProperties).map(value => new HierarchicalKind(value)); const sourceActions = new Map(); for (const contribution of contributions) { for (const action of contribution.actions) { const kind = new HierarchicalKind(action.kind); - if (CodeActionKind.Source.contains(kind) - // Exclude any we already included by default - && !defaultKinds.some(defaultKind => defaultKind.contains(kind)) - ) { + if (CodeActionKind.Source.contains(kind)) { sourceActions.set(kind.value, action); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 9e12975a5c9..36ac99f6654 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -21,6 +21,9 @@ class ToggleScreenReaderMode extends Action2 { super({ id: 'editor.action.toggleScreenReaderAccessibilityMode', title: nls.localize2('toggleScreenReaderMode', "Toggle Screen Reader Accessibility Mode"), + metadata: { + description: nls.localize2('toggleScreenReaderModeDescription', "Toggles an optimized mode for usage with screen readers, braille devices, and other assistive technologies."), + }, f1: true, keybinding: [{ primary: KeyMod.CtrlCmd | KeyCode.KeyE, diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index 2a96931a42c..bfef8956ae8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts @@ -11,7 +11,7 @@ import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { HasSpeechProvider, ISpeechService, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, SpeechToTextInProgress, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { Codicon } from 'vs/base/common/codicons'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditorAction2, EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -29,6 +29,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { toAction } from 'vs/base/common/actions'; import { ThemeIcon } from 'vs/base/common/themables'; +import { isWindows } from 'vs/base/common/platform'; const EDITOR_DICTATION_IN_PROGRESS = new RawContextKey('editorDictation.inProgress', false); const VOICE_CATEGORY = localize2('voiceCategory', "Voice"); @@ -40,11 +41,18 @@ export class EditorDictationStartAction extends EditorAction2 { id: 'workbench.action.editorDictation.start', title: localize2('startDictation', "Start Dictation in Editor"), category: VOICE_CATEGORY, - precondition: ContextKeyExpr.and(HasSpeechProvider, EDITOR_DICTATION_IN_PROGRESS.toNegated(), EditorContextKeys.readOnly.toNegated()), + precondition: ContextKeyExpr.and( + HasSpeechProvider, + SpeechToTextInProgress.toNegated(), // disable when any speech-to-text is in progress + EditorContextKeys.readOnly.toNegated() // disable in read-only editors + ), f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyV, - weight: KeybindingWeight.WorkbenchContrib + weight: KeybindingWeight.WorkbenchContrib, + secondary: isWindows ? [ + KeyMod.Alt | KeyCode.Backquote + ] : undefined } }); } @@ -182,7 +190,7 @@ export class EditorDictation extends Disposable implements IEditorContribution { private readonly widget = this._register(new DictationWidget(this.editor, this.keybindingService)); private readonly editorDictationInProgress = EDITOR_DICTATION_IN_PROGRESS.bindTo(this.contextKeyService); - private sessionDisposables = this._register(new MutableDisposable()); + private readonly sessionDisposables = this._register(new MutableDisposable()); constructor( private readonly editor: ICodeEditor, diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts new file mode 100644 index 00000000000..2f3564d2cb8 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { localize } from 'vs/nls'; +import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { getCommentCommandInfo } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'diff-editor'; + readonly when = ContextKeyEqualsExpr.create('isInDiffEditor', true); + readonly type = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + const codeEditorService = accessor.get(ICodeEditorService); + const keybindingService = accessor.get(IKeybindingService); + const contextKeyService = accessor.get(IContextKeyService); + + if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { + return; + } + + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + return; + } + + const switchSides = localize('msg3', "Run the command Diff Editor: Switch Side{0} to toggle between the original and modified editors.", ''); + const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); + + const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; + const content = [ + localize('msg1', "You are in a diff editor."), + localize('msg2', "View the next{0} or previous{1} diff in diff review mode, which is optimized for screen readers.", '', ''), + switchSides, + diffEditorActiveAnnouncement, + localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), + ]; + const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); + if (commentCommandInfo) { + content.push(commentCommandInfo); + } + return new AccessibleContentProvider( + AccessibleViewProviderId.DiffEditor, + { type: AccessibleViewType.Help }, + () => content.join('\n'), + () => codeEditor.focus(), + AccessibilityVerbositySettingId.DiffEditor, + ); + } +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index b47c9bc5642..15fba768221 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -3,29 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { autorunWithStore, observableFromEvent } from 'vs/base/common/observable'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; import { FloatingEditorClickWidget } from 'vs/workbench/browser/codeeditor'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { getCommentCommandInfo } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { DiffEditorAccessibilityHelp } from 'vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp'; class DiffEditorHelperContribution extends Disposable implements IDiffEditorContribution { public static readonly ID = 'editor.contrib.diffEditorHelper'; @@ -33,17 +25,15 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont constructor( private readonly _diffEditor: IDiffEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @INotificationService private readonly _notificationService: INotificationService, ) { super(); - this._register(createScreenReaderHelp()); - const isEmbeddedDiffEditor = this._diffEditor instanceof EmbeddedDiffEditorWidget; if (!isEmbeddedDiffEditor) { - const computationResult = observableFromEvent(e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); + const computationResult = observableFromEvent(this, e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); const onlyWhiteSpaceChange = computationResult.map(r => r && !r.identical && r.changes2.length === 0); this._register(autorunWithStore((reader, store) => { @@ -56,7 +46,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont null )); store.add(helperWidget.onClick(() => { - this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); + this._textResourceConfigurationService.updateValue(this._diffEditor.getModel()!.modified.uri, 'diffEditor.ignoreTrimWhitespace', false); })); helperWidget.render(); } @@ -72,7 +62,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont [{ label: localize('removeTimeout', "Remove Limit"), run: () => { - this._configurationService.updateValue('diffEditor.maxComputationTime', 0); + this._textResourceConfigurationService.updateValue(this._diffEditor.getModel()!.modified.uri, 'diffEditor.maxComputationTime', 0); } }], {} @@ -83,59 +73,6 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont } } -function createScreenReaderHelp(): IDisposable { - return AccessibilityHelpAction.addImplementation(105, 'diff-editor', async (accessor) => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const editorService = accessor.get(IEditorService); - const codeEditorService = accessor.get(ICodeEditorService); - const keybindingService = accessor.get(IKeybindingService); - const contextKeyService = accessor.get(IContextKeyService); - - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { - return; - } - - const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!codeEditor) { - return; - } - - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - let switchSides; - const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); - if (switchSidesKb) { - switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); - } else { - switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); - } - - const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); - - const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; - const content = [ - localize('msg1', "You are in a diff editor."), - localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), - switchSides, - diffEditorActiveAnnouncement, - localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), - ]; - const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); - if (commentCommandInfo) { - content.push(commentCommandInfo); - } - accessibleViewService.show({ - id: AccessibleViewProviderId.DiffEditor, - verbositySettingKey: AccessibilityVerbositySettingId.DiffEditor, - provideContent: () => content.join('\n\n'), - onClose: () => { - codeEditor.focus(); - }, - options: { type: AccessibleViewType.Help } - }); - }, ContextKeyEqualsExpr.create('isInDiffEditor', true)); -} - registerDiffEditorContribution(DiffEditorHelperContribution.ID, DiffEditorHelperContribution); Registry.as(Extensions.ConfigurationMigration) @@ -148,3 +85,4 @@ Registry.as(Extensions.ConfigurationMigration) ]; } }]); +AccessibleViewRegistry.register(new DiffEditorAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index ab46e943663..952a32eac79 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -22,7 +22,6 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IContentActionHandler, renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { IInlineChatService, IInlineChatSessionProvider } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -30,32 +29,16 @@ import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLa import { OS } from 'vs/base/common/platform'; import { status } from 'vs/base/browser/ui/aria/aria'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common/output'; import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { ChatAgentLocation, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; const $ = dom.$; -// TODO@joyceerhl remove this after a few iterations -Registry.as(Extensions.ConfigurationMigration) - .registerConfigurationMigrations([{ - key: 'workbench.editor.untitled.hint', - migrateFn: (value, _accessor) => ([ - [emptyTextEditorHintSetting, { value }], - ['workbench.editor.untitled.hint', { value: undefined }] - ]) - }, - { - key: 'accessibility.verbosity.untitledHint', - migrateFn: (value, _accessor) => ([ - [AccessibilityVerbositySettingId.EmptyEditorHint, { value }], - ['accessibility.verbosity.untitledHint', { value: undefined }] - ]) - }]); - export interface IEmptyTextEditorHintOptions { readonly clickable?: boolean; } @@ -73,17 +56,19 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService protected readonly configurationService: IConfigurationService, + @IHoverService protected readonly hoverService: IHoverService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IInlineChatSessionService private readonly inlineChatSessionService: IInlineChatSessionService, - @IInlineChatService protected readonly inlineChatService: IInlineChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService protected readonly productService: IProductService, + @IContextMenuService private readonly contextMenuService: IContextMenuService ) { this.toDispose = []; this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelContent(() => this.update())); - this.toDispose.push(this.inlineChatService.onDidChangeProviders(() => this.update())); + this.toDispose.push(this.chatAgentService.onDidChangeAgents(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.update())); this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.readOnly)) { @@ -145,9 +130,9 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { return false; } - const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; - const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && !inlineChatProviders.length; - return inlineChatProviders.length > 0 || shouldRenderDefaultHint; + const hasEditorAgents = Boolean(this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); + const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID; + return hasEditorAgents || shouldRenderDefaultHint; } protected update(): void { @@ -159,10 +144,12 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.editorGroupsService, this.commandService, this.configurationService, + this.hoverService, this.keybindingService, - this.inlineChatService, + this.chatAgentService, this.telemetryService, - this.productService + this.productService, + this.contextMenuService ); } else if (!shouldRenderHint && this.textHintContentWidget) { this.textHintContentWidget.dispose(); @@ -181,7 +168,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { private static readonly ID = 'editor.widget.emptyHint'; private domNode: HTMLElement | undefined; - private toDispose: DisposableStore; + private readonly toDispose: DisposableStore; private isVisible = false; private ariaLabel: string = ''; @@ -191,10 +178,12 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { private readonly editorGroupsService: IEditorGroupsService, private readonly commandService: ICommandService, private readonly configurationService: IConfigurationService, + private readonly hoverService: IHoverService, private readonly keybindingService: IKeybindingService, - private readonly inlineChatService: IInlineChatService, + private readonly chatAgentService: IChatAgentService, private readonly telemetryService: ITelemetryService, - private readonly productService: IProductService + private readonly productService: IProductService, + private readonly contextMenuService: IContextMenuService, ) { this.toDispose = new DisposableStore(); this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { @@ -215,8 +204,38 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { return EmptyTextEditorHintContentWidget.ID; } - private _getHintInlineChat(providers: IInlineChatSessionProvider[]) { - const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort; + private _disableHint(e?: MouseEvent) { + const disableHint = () => { + this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); + this.dispose(); + this.editor.focus(); + }; + + if (!e) { + disableHint(); + return; + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, + getActions: () => { + return [{ + id: 'workench.action.disableEmptyEditorHint', + label: localize('disableEditorEmptyHint', "Disable Empty Editor Hint"), + tooltip: localize('disableEditorEmptyHint', "Disable Empty Editor Hint"), + enabled: true, + class: undefined, + run: () => { + disableHint(); + } + } + ]; + } + }); + } + + private _getHintInlineChat(providers: IChatAgent[]) { + const providerName = (providers.length === 1 ? providers[0].fullName : undefined) ?? this.productService.nameShort; const inlineChatId = 'inlineChat.start'; let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; @@ -254,6 +273,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { const hintPart = $('a', undefined, fragment); hintPart.style.fontStyle = 'italic'; hintPart.style.cursor = 'pointer'; + this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); return hintPart; } else { @@ -272,6 +292,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { if (this.options.clickable) { label.element.style.cursor = 'pointer'; + this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); } @@ -294,7 +315,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { hintElement.appendChild(rendered); } - return { ariaLabel, hintHandler, hintElement }; + return { ariaLabel, hintElement }; } private _getHintDefault() { @@ -312,7 +333,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { chooseEditorOnClickOrTap(event.browserEvent); break; case '3': - dontShowOnClickOrTap(); + this._disableHint(); break; } } @@ -327,7 +348,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { id: ChangeLanguageAction.ID, from: 'hint' }); - await this.commandService.executeCommand(ChangeLanguageAction.ID, { from: 'hint' }); + await this.commandService.executeCommand(ChangeLanguageAction.ID); this.editor.focus(); }; @@ -357,12 +378,6 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } }; - const dontShowOnClickOrTap = () => { - this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); - this.dispose(); - this.editor.focus(); - }; - const hintMsg = localize({ key: 'message', comment: [ @@ -384,7 +399,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { anchor.style.cursor = 'pointer'; const id = keybindingsLookup.shift(); const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel(); - hintHandler.disposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); + hintHandler.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); } return { hintElement, ariaLabel }; @@ -396,7 +411,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { this.domNode.style.width = 'max-content'; this.domNode.style.paddingLeft = '4px'; - const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; + const inlineChatProviders = this.chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Editor)); const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders); this.domNode.append(hintElement); this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EmptyEditorHint)); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 5ba2522ed34..c7390207969 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -25,6 +25,7 @@ import { status } from 'vs/base/browser/ui/aria/aria'; import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import type { IHoverService } from 'vs/platform/hover/browser/hover'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -73,7 +74,8 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa options: IFindOptions, contextViewService: IContextViewService, contextKeyService: IContextKeyService, - private readonly _keybindingService: IKeybindingService + hoverService: IHoverService, + private readonly _keybindingService: IKeybindingService, ) { super(); @@ -143,7 +145,7 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa onTrigger: () => { this.find(true); } - })); + }, hoverService)); this.nextBtn = this._register(new SimpleButton({ label: NLS_NEXT_MATCH_BTN_LABEL + (options.nextMatchActionId ? this._getKeybinding(options.nextMatchActionId) : ''), @@ -151,7 +153,7 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa onTrigger: () => { this.find(false); } - })); + }, hoverService)); const closeBtn = this._register(new SimpleButton({ label: NLS_CLOSE_BTN_LABEL + (options.closeWidgetActionId ? this._getKeybinding(options.closeWidgetActionId) : ''), @@ -159,7 +161,7 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa onTrigger: () => { this.hide(); } - })); + }, hoverService)); this._innerDomNode = document.createElement('div'); this._innerDomNode.classList.add('simple-find-part'); @@ -276,9 +278,7 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa override dispose() { super.dispose(); - if (this._domNode && this._domNode.parentElement) { - this._domNode.parentElement.removeChild(this._domNode); - } + this._domNode?.remove(); } public isVisible(): boolean { diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index e4d481e7fe1..8b2fa1d6632 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -326,7 +326,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (semanticTokenInfo.metadata[property] !== undefined) { const definition = semanticTokenInfo.definitions[property]; const defValue = this._renderTokenStyleDefinition(definition, property); - const defValueStr = defValue.map(el => el instanceof HTMLElement ? el.outerHTML : el).join(); + const defValueStr = defValue.map(el => dom.isHTMLElement(el) ? el.outerHTML : el).join(); let properties = propertiesByDefValue[defValueStr]; if (!properties) { propertiesByDefValue[defValueStr] = properties = []; diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 71a1b593698..ab35c780ddc 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -118,12 +118,12 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return []; } - return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token); + return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token, model); } //#endregion - protected override provideWithoutTextEditor(picker: IQuickPick): IDisposable { + protected override provideWithoutTextEditor(picker: IQuickPick): IDisposable { if (this.canPickWithOutlineService()) { return this.doGetOutlinePicks(picker); } @@ -134,7 +134,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return this.editorService.activeEditorPane ? this.outlineService.canCreateOutline(this.editorService.activeEditorPane) : false; } - private doGetOutlinePicks(picker: IQuickPick): IDisposable { + private doGetOutlinePicks(picker: IQuickPick): IDisposable { const pane = this.editorService.activeEditorPane; if (!pane) { return Disposable.None; diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index d04fd459483..b7e5e721a28 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; @@ -30,6 +30,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITextFileEditorModel, ITextFileSaveParticipant, ITextFileSaveParticipantContext, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -267,13 +269,53 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { } } -class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { +class CodeActionOnSaveParticipant extends Disposable implements ITextFileSaveParticipant { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - ) { } + @IHostService private readonly hostService: IHostService, + @IEditorService private readonly editorService: IEditorService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + ) { + super(); + + this._register(this.hostService.onDidChangeFocus(() => { this.triggerCodeActionsCommand(); })); + this._register(this.editorService.onDidActiveEditorChange(() => { this.triggerCodeActionsCommand(); })); + } + + private async triggerCodeActionsCommand() { + if (this.configurationService.getValue('editor.codeActions.triggerOnFocusChange') && this.configurationService.getValue('files.autoSave') === 'afterDelay') { + const model = this.codeEditorService.getActiveCodeEditor()?.getModel(); + if (!model) { + return undefined; + } + + const settingsOverrides = { overrideIdentifier: model.getLanguageId(), resource: model.uri }; + const setting = this.configurationService.getValue<{ [kind: string]: string | boolean } | string[]>('editor.codeActionsOnSave', settingsOverrides); + + if (!setting) { + return undefined; + } + + if (Array.isArray(setting)) { + return undefined; + } + + const settingItems: string[] = Object.keys(setting).filter(x => setting[x] && setting[x] === 'always' && CodeActionKind.Source.contains(new HierarchicalKind(x))); + + const cancellationTokenSource = new CancellationTokenSource(); + + const codeActionKindList = []; + for (const item of settingItems) { + codeActionKindList.push(new HierarchicalKind(item)); + } + + // run code actions based on what is found from setting === 'always', no exclusions. + await this.applyOnSaveActions(model, codeActionKindList, [], Progress.None, cancellationTokenSource.token); + } + } async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { if (!model.textEditorModel) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts index b4f68265026..912b9a49299 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts @@ -13,6 +13,9 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { selectionBackground, inputBackground, inputForeground, editorSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; export function getSimpleEditorOptions(configurationService: IConfigurationService): IEditorOptions { return { @@ -25,7 +28,8 @@ export function getSimpleEditorOptions(configurationService: IConfigurationServi hideCursorInOverviewRuler: true, selectionHighlight: false, scrollbar: { - horizontal: 'hidden' + horizontal: 'hidden', + alwaysConsumeMouseWheel: false }, lineDecorationsWidth: 0, overviewRulerBorder: false, @@ -59,3 +63,35 @@ export function getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { ]) }; } + +/** + * Should be called to set the styling on editors that are appearing as just input boxes + * @param editorContainerSelector An element selector that will match the container of the editor + */ +export function setupSimpleEditorSelectionStyling(editorContainerSelector: string): IDisposable { + // Override styles in selections.ts + return registerThemingParticipant((theme, collector) => { + const selectionBackgroundColor = theme.getColor(selectionBackground); + + if (selectionBackgroundColor) { + // Override inactive selection bg + const inputBackgroundColor = theme.getColor(inputBackground); + if (inputBackgroundColor) { + collector.addRule(`${editorContainerSelector} .monaco-editor-background { background-color: ${inputBackgroundColor}; } `); + collector.addRule(`${editorContainerSelector} .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`); + } + + // Override selected fg + const inputForegroundColor = theme.getColor(inputForeground); + if (inputForegroundColor) { + collector.addRule(`${editorContainerSelector} .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`); + } + + collector.addRule(`${editorContainerSelector} .monaco-editor .focused .selected-text { background-color: ${selectionBackgroundColor}; }`); + } else { + // Use editor selection color if theme has not set a selection background color + collector.addRule(`${editorContainerSelector} .monaco-editor .focused .selected-text { background-color: ${theme.getColor(editorSelectionBackground)}; }`); + } + }); + +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 6fbc04ff214..64bfec8be9d 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -3,42 +3,41 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./suggestEnabledInput'; import { $, Dimension, append } from 'vs/base/browser/dom'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; +import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { Widget } from 'vs/base/browser/ui/widget'; import { Emitter, Event } from 'vs/base/common/event'; +import { HistoryNavigator } from 'vs/base/common/history'; import { KeyCode } from 'vs/base/common/keyCodes'; import { mixin } from 'vs/base/common/objects'; import { isMacintosh } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; +import 'vs/css!./suggestEnabledInput'; +import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; +import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/core/wordHelper'; import * as languages from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IModelService } from 'vs/editor/common/services/model'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ColorIdentifier, asCssVariable, asCssVariableWithDefault, editorSelectionBackground, inputBackground, inputBorder, inputForeground, inputPlaceholderForeground, selectionBackground } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; -import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; -import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; -import { HistoryNavigator } from 'vs/base/common/history'; -import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; -import { IHistoryNavigationWidget } from 'vs/base/browser/history'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; -import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/core/wordHelper'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IHistoryNavigationContext, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ColorIdentifier, asCssVariable, asCssVariableWithDefault, inputBackground, inputBorder, inputForeground, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; export interface SuggestResultsProvider { /** @@ -162,7 +161,7 @@ export class SuggestEnabledInput extends Widget { const scopedContextKeyService = this.getScopedContextKeyService(contextKeyService); const instantiationService = scopedContextKeyService - ? defaultInstantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])) + ? this._register(defaultInstantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))) : defaultInstantiationService; this.inputWidget = this._register(instantiationService.createInstance(CodeEditorWidget, this.stylingContainer, @@ -462,34 +461,7 @@ export class ContextScopedSuggestEnabledInputWithHistory extends SuggestEnabledI } } -// Override styles in selections.ts -registerThemingParticipant((theme, collector) => { - const selectionBackgroundColor = theme.getColor(selectionBackground); - - if (selectionBackgroundColor) { - // Override inactive selection bg - const inputBackgroundColor = theme.getColor(inputBackground); - if (inputBackgroundColor) { - collector.addRule(`.suggest-input-container .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`); - } - - // Override selected fg - const inputForegroundColor = theme.getColor(inputForeground); - if (inputForegroundColor) { - collector.addRule(`.suggest-input-container .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`); - } - - const backgroundColor = theme.getColor(inputBackground); - if (backgroundColor) { - collector.addRule(`.suggest-input-container .monaco-editor-background { background-color: ${backgroundColor}; } `); - } - collector.addRule(`.suggest-input-container .monaco-editor .focused .selected-text { background-color: ${selectionBackgroundColor}; }`); - } else { - // Use editor selection color if theme has not set a selection background color - collector.addRule(`.suggest-input-container .monaco-editor .focused .selected-text { background-color: ${theme.getColor(editorSelectionBackground)}; }`); - } -}); - +setupSimpleEditorSelectionStyling('.suggest-input-container'); function getSuggestEnabledInputOptions(ariaLabel?: string): IEditorOptions { return { diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index f5b2c84ebcb..5b5081623cf 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; +import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class ToggleMultiCursorModifierAction extends Action2 { @@ -39,7 +40,7 @@ export class ToggleMultiCursorModifierAction extends Action2 { const multiCursorModifier = new RawContextKey('multiCursorModifier', 'altKey'); -class MultiCursorModifierContextKeyController implements IWorkbenchContribution { +class MultiCursorModifierContextKeyController extends Disposable implements IWorkbenchContribution { private readonly _multiCursorModifier: IContextKey; @@ -47,14 +48,15 @@ class MultiCursorModifierContextKeyController implements IWorkbenchContribution @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService ) { + super(); this._multiCursorModifier = multiCursorModifier.bindTo(contextKeyService); this._update(); - configurationService.onDidChangeConfiguration((e) => { + this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('editor.multiCursorModifier')) { this._update(); } - }); + })); } private _update(): void { diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 7cdd7d910ad..043e3f5c9e7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -3,25 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { addDisposableListener, onDidRegisterWindow } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution, registerDiffEditorContribution, EditorContributionInstantiation } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorContributionInstantiation, ServicesAccessor, registerDiffEditorContribution, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IDiffEditorContribution, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { Codicon } from 'vs/base/common/codicons'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Event } from 'vs/base/common/event'; -import { addDisposableListener, onDidRegisterWindow } from 'vs/base/browser/dom'; -import { mainWindow } from 'vs/base/browser/window'; const transientWordWrapState = 'transientWordWrapState'; const isWordWrapMinifiedKey = 'isWordWrapMinified'; @@ -271,7 +271,7 @@ class EditorWordWrapContextKeyTracker extends Disposable implements IWorkbenchCo disposables.add(addDisposableListener(window, 'focus', () => this._update(), true)); disposables.add(addDisposableListener(window, 'blur', () => this._update(), true)); }, { window: mainWindow, disposables: this._store })); - this._editorService.onDidActiveEditorChange(() => this._update()); + this._register(this._editorService.onDidActiveEditorChange(() => this._update())); this._canToggleWordWrap = CAN_TOGGLE_WORD_WRAP.bindTo(this._contextService); this._editorWordWrap = EDITOR_WORD_WRAP.bindTo(this._contextService); this._activeEditor = null; diff --git a/src/vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate.ts index 653f28e4d15..65773d24025 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-sandbox/startDebugTextMate.ts @@ -29,7 +29,7 @@ class StartDebugTextMate extends Action2 { constructor() { super({ id: 'editor.action.startDebugTextMate', - title: nls.localize2('startDebugTextMate', "Start Text Mate Syntax Grammar Logging"), + title: nls.localize2('startDebugTextMate', "Start TextMate Syntax Grammar Logging"), category: Categories.Developer, f1: true }); diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index f8960304793..42cc4253ad2 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FinalNewLineParticipant, TrimFinalNewLinesParticipant, TrimWhitespaceParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 6d58f57ebba..1bd9688ea69 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -5,8 +5,8 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import assert from 'assert'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { getReindentEditOperations } from 'vs/editor/contrib/indentation/common/indentation'; @@ -15,6 +15,15 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { ILanguageConfiguration, LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; import { parse } from 'vs/base/common/json'; import { IRange } from 'vs/editor/common/core/range'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; +import { execSync } from 'child_process'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; +import { NullState } from 'vs/editor/common/languages/nullTokenize'; +import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ITextModel } from 'vs/editor/common/model'; +import { FileAccess } from 'vs/base/common/network'; function getIRange(range: IRange): IRange { return { @@ -25,9 +34,70 @@ function getIRange(range: IRange): IRange { }; } +const enum LanguageId { + TypeScript = 'ts-test' +} + +function forceTokenizationFromLineToLine(model: ITextModel, startLine: number, endLine: number): void { + for (let line = startLine; line <= endLine; line++) { + model.tokenization.forceTokenization(line); + } +} + +function registerLanguage(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable { + const disposables = new DisposableStore(); + const languageService = instantiationService.get(ILanguageService); + disposables.add(registerLanguageConfiguration(instantiationService, languageId)); + disposables.add(languageService.registerLanguage({ id: languageId })); + return disposables; +} + +function registerLanguageConfiguration(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable { + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + let configPath: string; + switch (languageId) { + case LanguageId.TypeScript: + configPath = FileAccess.asFileUri('vs/workbench/contrib/codeEditor/test/node/language-configuration.json').fsPath; + break; + default: + throw new Error('Unknown languageId'); + } + const configContent = fs.readFileSync(configPath, { encoding: 'utf-8' }); + const parsedConfig = parse(configContent, []); + const languageConfig = LanguageConfigurationFileHandler.extractValidConfig(languageId, parsedConfig); + return languageConfigurationService.register(languageId, languageConfig); +} + +interface StandardTokenTypeData { + startIndex: number; + standardTokenType: StandardTokenType; +} + +function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: LanguageId): IDisposable { + let lineIndex = 0; + const languageService = instantiationService.get(ILanguageService); + const tokenizationSupport: ITokenizationSupport = { + getInitialState: () => NullState, + tokenize: undefined!, + tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { + const tokensOnLine = tokens[lineIndex++]; + const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId); + const result = new Uint32Array(2 * tokensOnLine.length); + for (let i = 0; i < tokensOnLine.length; i++) { + result[2 * i] = tokensOnLine[i].startIndex; + result[2 * i + 1] = + ((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) + | (tokensOnLine[i].standardTokenType << MetadataConsts.TOKEN_TYPE_OFFSET)); + } + return new EncodedTokenizationResult(result, state); + } + }; + return TokenizationRegistry.register(languageId, tokenizationSupport); +} + suite('Auto-Reindentation - TypeScript/JavaScript', () => { - const languageId = 'ts-test'; + const languageId = LanguageId.TypeScript; const options: IRelaxedTextModelCreationOptions = {}; let disposables: DisposableStore; let instantiationService: TestInstantiationService; @@ -37,11 +107,9 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { disposables = new DisposableStore(); instantiationService = createModelServices(disposables); languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - const configPath = path.join('extensions', 'typescript-basics', 'language-configuration.json'); - const configString = fs.readFileSync(configPath).toString(); - const config = parse(configString, []); - const configParsed = LanguageConfigurationFileHandler.extractValidConfig(languageId, config); - disposables.add(languageConfigurationService.register(languageId, configParsed)); + disposables.add(instantiationService); + disposables.add(registerLanguage(instantiationService, languageId)); + disposables.add(registerLanguageConfiguration(instantiationService, languageId)); }); teardown(() => { @@ -51,20 +119,64 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ensureNoDisposablesAreLeakedInTestSuite(); // Test which can be ran to find cases of incorrect indentation... - test.skip('Find Cases of Incorrect Indentation', () => { + test.skip('Find Cases of Incorrect Indentation with the Reindent Lines Command', () => { - const filePath = path.join('..', 'TypeScript', 'src', 'server', 'utilities.ts'); - const fileContents = fs.readFileSync(filePath).toString(); + // ./scripts/test.sh --inspect --grep='Find Cases of Incorrect Indentation with the Reindent Lines Command' --timeout=15000 - const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); - const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); - model.applyEdits(editOperations); + function walkDirectoryAndReindent(directory: string, languageId: string) { + const files = fs.readdirSync(directory, { withFileTypes: true }); + const directoriesToRecurseOn: string[] = []; + for (const file of files) { + if (file.isDirectory()) { + directoriesToRecurseOn.push(path.join(directory, file.name)); + } else { + const filePathName = path.join(directory, file.name); + const fileExtension = path.extname(filePathName); + if (fileExtension !== '.ts') { + continue; + } + const fileContents = fs.readFileSync(filePathName, { encoding: 'utf-8' }); + const modelOptions: IRelaxedTextModelCreationOptions = { + tabSize: 4, + insertSpaces: false + }; + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, modelOptions)); + const lineCount = model.getLineCount(); + const editOperations: ISingleEditOperation[] = []; + for (let line = 1; line <= lineCount - 1; line++) { + /* + NOTE: Uncomment in order to ignore incorrect JS DOC indentation + const lineContent = model.getLineContent(line); + const trimmedLineContent = lineContent.trim(); + if (trimmedLineContent.length === 0 || trimmedLineContent.startsWith('*') || trimmedLineContent.startsWith('/*')) { + continue; + } + */ + const lineContent = model.getLineContent(line); + const trimmedLineContent = lineContent.trim(); + if (trimmedLineContent.length === 0) { + continue; + } + const editOperation = getReindentEditOperations(model, languageConfigurationService, line, line + 1); + /* + NOTE: Uncomment in order to see actual incorrect indentation diff + model.applyEdits(editOperation); + */ + editOperations.push(...editOperation); + } + model.applyEdits(editOperations); + model.applyEdits(trimTrailingWhitespace(model, [], true)); + fs.writeFileSync(filePathName, model.getValue()); + } + } + for (const directory of directoriesToRecurseOn) { + walkDirectoryAndReindent(directory, languageId); + } + } - // save the files to disk - const initialFile = path.join('..', 'autoindent', 'initial.ts'); - const finalFile = path.join('..', 'autoindent', 'final.ts'); - fs.writeFileSync(initialFile, fileContents); - fs.writeFileSync(finalFile, model.getValue()); + walkDirectoryAndReindent('/Users/aiday/Desktop/Test/vscode-test', 'ts-test'); + const output = execSync('cd /Users/aiday/Desktop/Test/vscode-test && git diff --shortstat', { encoding: 'utf-8' }); + console.log('\ngit diff --shortstat:\n', output); }); // Unit tests for increase and decrease indent patterns... @@ -98,7 +210,27 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { 'const foo = `{`;', ' ', ].join('\n'); + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 5, standardTokenType: StandardTokenType.Other }, + { startIndex: 6, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.Other }, + { startIndex: 10, standardTokenType: StandardTokenType.Other }, + { startIndex: 11, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.String }, + { startIndex: 13, standardTokenType: StandardTokenType.String }, + { startIndex: 14, standardTokenType: StandardTokenType.String }, + { startIndex: 15, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 4, standardTokenType: StandardTokenType.Other }] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + forceTokenizationFromLineToLine(model, 1, 2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; @@ -196,7 +328,28 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { 'const r = /{/;', ' ', ].join('\n'); + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 5, standardTokenType: StandardTokenType.Other }, + { startIndex: 6, standardTokenType: StandardTokenType.Other }, + { startIndex: 7, standardTokenType: StandardTokenType.Other }, + { startIndex: 8, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 10, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 11, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 12, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 13, standardTokenType: StandardTokenType.Other }, + { startIndex: 14, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 4, standardTokenType: StandardTokenType.Other } + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + forceTokenizationFromLineToLine(model, 1, 2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; @@ -230,6 +383,38 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { assert.deepStrictEqual(editOperations.length, 0); }); + test('Issue #209859: do not do reindentation for tokens inside of a string', () => { + + // issue: https://github.com/microsoft/vscode/issues/209859 + + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const fileContents = [ + 'const foo = `some text', + ' which is strangely', + ' indented. It should', + ' not be reindented.`' + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + forceTokenizationFromLineToLine(model, 1, 4); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + // Failing tests inferred from the current regexes... test.skip('Incorrect deindentation after `*/}` string', () => { diff --git a/src/vs/workbench/contrib/codeEditor/test/node/language-configuration.json b/src/vs/workbench/contrib/codeEditor/test/node/language-configuration.json new file mode 100644 index 00000000000..25a23685738 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/test/node/language-configuration.json @@ -0,0 +1,250 @@ +{ + // Note that this file should stay in sync with 'javascript-language-basics/javascript-language-configuration.json' + "comments": { + "lineComment": "//", + "blockComment": [ + "/*", + "*/" + ] + }, + "brackets": [ + [ + "${", + "}" + ], + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] + ], + "autoClosingPairs": [ + { + "open": "{", + "close": "}" + }, + { + "open": "[", + "close": "]" + }, + { + "open": "(", + "close": ")" + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "`", + "close": "`", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "/**", + "close": " */", + "notIn": [ + "string" + ] + } + ], + "surroundingPairs": [ + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "'", + "'" + ], + [ + "\"", + "\"" + ], + [ + "`", + "`" + ], + [ + "<", + ">" + ] + ], + "colorizedBracketPairs": [ + [ + "(", + ")" + ], + [ + "[", + "]" + ], + [ + "{", + "}" + ], + [ + "<", + ">" + ] + ], + "autoCloseBefore": ";:.,=}])>` \n\t", + "folding": { + "markers": { + "start": "^\\s*//\\s*#?region\\b", + "end": "^\\s*//\\s*#?endregion\\b" + } + }, + "wordPattern": { + "pattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\@\\~\\!\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>/\\?\\s]+)", + }, + "indentationRules": { + "decreaseIndentPattern": { + "pattern": "^\\s*[\\}\\]\\)].*$" + }, + "increaseIndentPattern": { + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" + }, + // e.g. * ...| or */| or *-----*/| + "unIndentedLinePattern": { + "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*\\*([ ]([^\\*]|\\*(?!/))*)?$" + }, + "indentNextLinePattern": { + "pattern": "^((.*=>\\s*)|((.*[^\\w]+|\\s*)(if|while|for)\\s*\\(.*\\)\\s*))$" + } + }, + "onEnterRules": [ + { + // e.g. /** | */ + "beforeText": { + "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" + }, + "afterText": { + "pattern": "^\\s*\\*/$" + }, + "action": { + "indent": "indentOutdent", + "appendText": " * " + } + }, + { + // e.g. /** ...| + "beforeText": { + "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" + }, + "action": { + "indent": "none", + "appendText": " * " + } + }, + { + // e.g. * ...| + "beforeText": { + "pattern": "^(\\t|[ ])*\\*([ ]([^\\*]|\\*(?!/))*)?$" + }, + "previousLineText": { + "pattern": "(?=^(\\s*(/\\*\\*|\\*)).*)(?=(?!(\\s*\\*/)))" + }, + "action": { + "indent": "none", + "appendText": "* " + } + }, + { + // e.g. */| + "beforeText": { + "pattern": "^(\\t|[ ])*[ ]\\*/\\s*$" + }, + "action": { + "indent": "none", + "removeText": 1 + }, + }, + { + // e.g. *-----*/| + "beforeText": { + "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$" + }, + "action": { + "indent": "none", + "removeText": 1 + }, + }, + { + "beforeText": { + "pattern": "^\\s*(\\bcase\\s.+:|\\bdefault:)$" + }, + "afterText": { + "pattern": "^(?!\\s*(\\bcase\\b|\\bdefault\\b))" + }, + "action": { + "indent": "indent" + } + }, + { + // Decrease indentation after single line if/else if/else, for, or while + "previousLineText": "^\\s*(((else ?)?if|for|while)\\s*\\(.*\\)\\s*|else\\s*)$", + // But make sure line doesn't have braces or is not another if statement + "beforeText": "^\\s+([^{i\\s]|i(?!f\\b))", + "action": { + "indent": "outdent" + } + }, + // Indent when pressing enter from inside () + { + "beforeText": "^.*\\([^\\)]*$", + "afterText": "^\\s*\\).*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside {} + { + "beforeText": "^.*\\{[^\\}]*$", + "afterText": "^\\s*\\}.*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + // Indent when pressing enter from inside [] + { + "beforeText": "^.*\\[[^\\]]*$", + "afterText": "^\\s*\\].*$", + "action": { + "indent": "indentOutdent", + "appendText": "\t", + } + }, + ] +} diff --git a/src/vs/workbench/contrib/commands/common/commands.contribution.ts b/src/vs/workbench/contrib/commands/common/commands.contribution.ts index c4d4a5b3649..55cf296c7f2 100644 --- a/src/vs/workbench/contrib/commands/common/commands.contribution.ts +++ b/src/vs/workbench/contrib/commands/common/commands.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { safeStringify } from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -99,14 +100,14 @@ class RunCommands extends Action2 { const cmd = args.commands[i]; - logService.debug(`runCommands: executing ${i}-th command: ${JSON.stringify(cmd)}`); + logService.debug(`runCommands: executing ${i}-th command: ${safeStringify(cmd)}`); - const r = await this._runCommand(commandService, cmd); + await this._runCommand(commandService, cmd); - logService.debug(`runCommands: executed ${i}-th command with return value: ${JSON.stringify(r)}`); + logService.debug(`runCommands: executed ${i}-th command`); } } catch (err) { - logService.debug(`runCommands: executing ${i}-th command resulted in an error: ${err instanceof Error ? err.message : JSON.stringify(err)}`); + logService.debug(`runCommands: executing ${i}-th command resulted in an error: ${err instanceof Error ? err.message : safeStringify(err)}`); notificationService.error(err); } diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts index 08d44a3224b..b66b9590f76 100644 --- a/src/vs/workbench/contrib/comments/browser/commentColors.ts +++ b/src/vs/workbench/contrib/comments/browser/commentColors.ts @@ -13,11 +13,11 @@ import { IColorTheme } from 'vs/platform/theme/common/themeService'; const resolvedCommentViewIcon = registerColor('commentsView.resolvedIcon', { dark: disabledForeground, light: disabledForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentIcon', 'Icon color for resolved comments.')); const unresolvedCommentViewIcon = registerColor('commentsView.unresolvedIcon', { dark: listFocusOutline, light: listFocusOutline, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentIcon', 'Icon color for unresolved comments.')); -registerColor('editorCommentsWidget.replyInputBackground', { dark: peekViewTitleBackground, light: peekViewTitleBackground, hcDark: peekViewTitleBackground, hcLight: peekViewTitleBackground }, nls.localize('commentReplyInputBackground', 'Background color for comment reply input box.')); +registerColor('editorCommentsWidget.replyInputBackground', peekViewTitleBackground, nls.localize('commentReplyInputBackground', 'Background color for comment reply input box.')); const resolvedCommentBorder = registerColor('editorCommentsWidget.resolvedBorder', { dark: resolvedCommentViewIcon, light: resolvedCommentViewIcon, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); const unresolvedCommentBorder = registerColor('editorCommentsWidget.unresolvedBorder', { dark: unresolvedCommentViewIcon, light: unresolvedCommentViewIcon, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); -export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); -export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); +export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', transparent(unresolvedCommentBorder, .1), nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); +export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', transparent(unresolvedCommentBorder, .1), nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); const commentThreadStateBorderColors = new Map([ [languages.CommentThreadState.Unresolved, unresolvedCommentBorder], diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 92b52ac5402..725b522f956 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -14,11 +14,11 @@ import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { CommentThreadState } from 'vs/editor/common/languages'; export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges. This color should be opaque.')); -const overviewRulerCommentForeground = registerColor('editorOverviewRuler.commentForeground', { dark: overviewRulerCommentingRangeForeground, light: overviewRulerCommentingRangeForeground, hcDark: overviewRulerCommentingRangeForeground, hcLight: overviewRulerCommentingRangeForeground }, nls.localize('editorOverviewRuler.commentForeground', 'Editor overview ruler decoration color for resolved comments. This color should be opaque.')); -const overviewRulerCommentUnresolvedForeground = registerColor('editorOverviewRuler.commentUnresolvedForeground', { dark: overviewRulerCommentForeground, light: overviewRulerCommentForeground, hcDark: overviewRulerCommentForeground, hcLight: overviewRulerCommentForeground }, nls.localize('editorOverviewRuler.commentUnresolvedForeground', 'Editor overview ruler decoration color for unresolved comments. This color should be opaque.')); +const overviewRulerCommentForeground = registerColor('editorOverviewRuler.commentForeground', overviewRulerCommentingRangeForeground, nls.localize('editorOverviewRuler.commentForeground', 'Editor overview ruler decoration color for resolved comments. This color should be opaque.')); +const overviewRulerCommentUnresolvedForeground = registerColor('editorOverviewRuler.commentUnresolvedForeground', overviewRulerCommentForeground, nls.localize('editorOverviewRuler.commentUnresolvedForeground', 'Editor overview ruler decoration color for unresolved comments. This color should be opaque.')); const editorGutterCommentGlyphForeground = registerColor('editorGutter.commentGlyphForeground', { dark: editorForeground, light: editorForeground, hcDark: Color.black, hcLight: Color.white }, nls.localize('editorGutterCommentGlyphForeground', 'Editor gutter decoration color for commenting glyphs.')); -registerColor('editorGutter.commentUnresolvedGlyphForeground', { dark: editorGutterCommentGlyphForeground, light: editorGutterCommentGlyphForeground, hcDark: editorGutterCommentGlyphForeground, hcLight: editorGutterCommentGlyphForeground }, nls.localize('editorGutterCommentUnresolvedGlyphForeground', 'Editor gutter decoration color for commenting glyphs for unresolved comment threads.')); +registerColor('editorGutter.commentUnresolvedGlyphForeground', editorGutterCommentGlyphForeground, nls.localize('editorGutterCommentUnresolvedGlyphForeground', 'Editor gutter decoration color for commenting glyphs for unresolved comment threads.')); export class CommentGlyphWidget { public static description = 'comment-glyph-widget'; diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index df2afb078e5..f7f6688cfbe 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -8,11 +8,8 @@ import * as dom from 'vs/base/browser/dom'; import * as languages from 'vs/editor/common/languages'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IActionRunner, IAction, Separator, ActionRunner } from 'vs/base/common/actions'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, IReference, dispose } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; @@ -45,12 +42,14 @@ import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { DomEmitter } from 'vs/base/browser/event'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; -import { FileAccess } from 'vs/base/common/network'; +import { FileAccess, Schemas } from 'vs/base/common/network'; import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MarshalledCommentThread } from 'vs/workbench/common/comments'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; class CommentsActionRunner extends ActionRunner { protected override async runAction(action: IAction, context: any[]): Promise { @@ -74,7 +73,7 @@ export class CommentNode extends Disposable { private _reactionActionsContainer?: HTMLElement; private _commentEditor: SimpleCommentEditor | null = null; private _commentEditorDisposables: IDisposable[] = []; - private _commentEditorModel: ITextModel | null = null; + private _commentEditorModel: IReference | null = null; private _editorHeight = MIN_EDITOR_HEIGHT; private _isPendingLabel!: HTMLElement; @@ -111,19 +110,19 @@ export class CommentNode extends Disposable { private markdownRenderer: MarkdownRenderer, @IInstantiationService private instantiationService: IInstantiationService, @ICommentService private commentService: ICommentService, - @IModelService private modelService: IModelService, - @ILanguageService private languageService: ILanguageService, @INotificationService private notificationService: INotificationService, @IContextMenuService private contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService private configurationService: IConfigurationService, + @IHoverService private hoverService: IHoverService, @IAccessibilityService private accessibilityService: IAccessibilityService, - @IKeybindingService private keybindingService: IKeybindingService + @IKeybindingService private keybindingService: IKeybindingService, + @ITextModelService private readonly textModelService: ITextModelService, ) { super(); this._domNode = dom.$('div.review-comment'); - this._contextKeyService = contextKeyService.createScoped(this._domNode); + this._contextKeyService = this._register(contextKeyService.createScoped(this._domNode)); this._commentContextValue = CommentContextKeys.commentContext.bindTo(this._contextKeyService); if (this.comment.contextValue) { this._commentContextValue.set(this.comment.contextValue); @@ -250,7 +249,7 @@ export class CommentNode extends Disposable { this._timestampWidget?.dispose(); } else { if (!this._timestampWidget) { - this._timestampWidget = new TimestampWidget(this.configurationService, this._timestamp, timestamp); + this._timestampWidget = new TimestampWidget(this.configurationService, this.hoverService, this._timestamp, timestamp); this._register(this._timestampWidget); } else { this._timestampWidget.setTimestamp(timestamp); @@ -492,13 +491,18 @@ export class CommentNode extends Disposable { return (typeof this.comment.body === 'string') ? this.comment.body : this.comment.body.value; } - private createCommentEditor(editContainer: HTMLElement): void { + private async createCommentEditor(editContainer: HTMLElement): Promise { const container = dom.append(editContainer, dom.$('.edit-textarea')); this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(this.configurationService), this._contextKeyService, this.parentThread); - const resource = URI.parse(`comment:commentinput-${this.comment.uniqueIdInThread}-${Date.now()}.md`); - this._commentEditorModel = this.modelService.createModel('', this.languageService.createByFilepathOrFirstLine(resource), resource, false); - this._commentEditor.setModel(this._commentEditorModel); + const resource = URI.from({ + scheme: Schemas.commentsInput, + path: `/commentinput-${this.comment.uniqueIdInThread}-${Date.now()}.md` + }); + const modelRef = await this.textModelService.createModelReference(resource); + this._commentEditorModel = modelRef; + + this._commentEditor.setModel(this._commentEditorModel.object.textEditorModel); this._commentEditor.setValue(this.pendingEdit ?? this.commentBodyValue); this.pendingEdit = undefined; this._commentEditor.layout({ width: container.clientWidth - 14, height: this._editorHeight }); @@ -509,8 +513,8 @@ export class CommentNode extends Disposable { this._commentEditor!.focus(); }); - const lastLine = this._commentEditorModel.getLineCount(); - const lastColumn = this._commentEditorModel.getLineLength(lastLine) + 1; + const lastLine = this._commentEditorModel.object.textEditorModel.getLineCount(); + const lastColumn = this._commentEditorModel.object.textEditorModel.getLineLength(lastLine) + 1; this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn)); const commentThread = this.commentThread; @@ -545,7 +549,7 @@ export class CommentNode extends Disposable { this.calculateEditorHeight(); - this._register((this._commentEditorModel.onDidChangeContent(() => { + this._register((this._commentEditorModel.object.textEditorModel.onDidChangeContent(() => { if (this._commentEditor && this.calculateEditorHeight()) { this._commentEditor.layout({ height: this._editorHeight, width: this._commentEditor.getLayoutInfo().width }); this._commentEditor.render(true); @@ -602,7 +606,7 @@ export class CommentNode extends Disposable { this._scrollableElement.setScrollDimensions({ width, scrollWidth, height, scrollHeight }); } - public switchToEditMode() { + public async switchToEditMode() { if (this.isEditing) { return; } @@ -610,7 +614,7 @@ export class CommentNode extends Disposable { this.isEditing = true; this._body.classList.add('hidden'); this._commentEditContainer = dom.append(this._commentDetailsContainer, dom.$('.edit-container')); - this.createCommentEditor(this._commentEditContainer); + await this.createCommentEditor(this._commentEditContainer); const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); const otherActions = dom.append(formActions, dom.$('.other-actions')); @@ -703,7 +707,7 @@ export class CommentNode extends Disposable { })); } - update(newComment: languages.Comment) { + async update(newComment: languages.Comment) { if (newComment.body !== this.comment.body) { this.updateCommentBody(newComment.body); @@ -719,7 +723,7 @@ export class CommentNode extends Disposable { if (isChangingMode) { if (newComment.mode === languages.CommentMode.Editing) { - this.switchToEditMode(); + await this.switchToEditMode(); } else { this.removeCommentEditor(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 55ea0d4e8f9..4f50bfb1085 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -4,24 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; import { IAction } from 'vs/base/common/actions'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IRange } from 'vs/editor/common/core/range'; import * as languages from 'vs/editor/common/languages'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; @@ -29,11 +29,8 @@ import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/comment import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { LayoutableEditor, MIN_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from './simpleCommentEditor'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; -const COMMENT_SCHEME = 'comment'; let INMEM_MODEL_ID = 0; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; @@ -42,8 +39,8 @@ export class CommentReply extends Disposable { form: HTMLElement; commentEditorIsEmpty: IContextKey; private _error!: HTMLElement; - private _formActions: HTMLElement | null; - private _editorActions: HTMLElement | null; + private _formActions!: HTMLElement; + private _editorActions!: HTMLElement; private _commentThreadDisposables: IDisposable[] = []; private _commentFormActions!: CommentFormActions; private _commentEditorActions!: CommentFormActions; @@ -61,13 +58,13 @@ export class CommentReply extends Disposable { private _commentOptions: languages.CommentOptions | undefined, private _pendingComment: string | undefined, private _parentThread: ICommentThreadWidget, + focus: boolean, private _actionRunDelegate: (() => void) | null, @ICommentService private commentService: ICommentService, - @ILanguageService private languageService: ILanguageService, - @IModelService private modelService: IModelService, - @IThemeService private themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService private keybindingService: IKeybindingService + @IKeybindingService private keybindingService: IKeybindingService, + @IHoverService private hoverService: IHoverService, + @ITextModelService private readonly textModelService: ITextModelService ) { super(); @@ -76,6 +73,10 @@ export class CommentReply extends Disposable { this.commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); this.commentEditorIsEmpty.set(!this._pendingComment); + this.initialize(focus); + } + + async initialize(focus: boolean) { const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); const params = JSON.stringify({ @@ -83,42 +84,49 @@ export class CommentReply extends Disposable { commentThreadId: this._commentThread.threadId }); - let resource = URI.parse(`${COMMENT_SCHEME}://${this._commentThread.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority. - const commentController = this.commentService.getCommentController(owner); + let resource = URI.from({ + scheme: Schemas.commentsInput, + path: `/${this._commentThread.extensionId}/commentinput-${modeId}.md?${params}` // TODO. Remove params once extensions adopt authority. + }); + const commentController = this.commentService.getCommentController(this.owner); if (commentController) { resource = resource.with({ authority: commentController.id }); } - const model = this.modelService.createModel(this._pendingComment || '', this.languageService.createByFilepathOrFirstLine(resource), resource, false); + const model = await this.textModelService.createModelReference(resource); + model.object.textEditorModel.setValue(this._pendingComment || ''); + this._register(model); - this.commentEditor.setModel(model); + this.commentEditor.setModel(model.object.textEditorModel); this.calculateEditorHeight(); - this._register((model.onDidChangeContent(() => { + this._register(model.object.textEditorModel.onDidChangeContent(() => { this.setCommentEditorDecorations(); this.commentEditorIsEmpty?.set(!this.commentEditor.getValue()); if (this.calculateEditorHeight()) { this.commentEditor.layout({ height: this._editorHeight, width: this.commentEditor.getLayoutInfo().width }); this.commentEditor.render(true); } - }))); + })); this.createTextModelListener(this.commentEditor, this.form); this.setCommentEditorDecorations(); // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments - if (hasExistingComments) { + if (this._pendingComment) { + this.expandReplyArea(); + } else if (hasExistingComments) { this.createReplyButton(this.commentEditor, this.form); - } else if ((this._commentThread.comments && this._commentThread.comments.length === 0) || this._pendingComment) { + } else if (focus && (this._commentThread.comments && this._commentThread.comments.length === 0)) { this.expandReplyArea(); } this._error = dom.append(this.form, dom.$('.validation-error.hidden')); const formActions = dom.append(this.form, dom.$('.form-actions')); this._formActions = dom.append(formActions, dom.$('.other-actions')); - this.createCommentWidgetFormActions(this._formActions, model); + this.createCommentWidgetFormActions(this._formActions, model.object.textEditorModel); this._editorActions = dom.append(formActions, dom.$('.editor-actions')); - this.createCommentWidgetEditorActions(this._editorActions, model); + this.createCommentWidgetEditorActions(this._editorActions, model.object.textEditorModel); } private calculateEditorHeight(): boolean { @@ -132,12 +140,13 @@ export class CommentReply extends Disposable { public updateCommentThread(commentThread: languages.CommentThread) { const isReplying = this.commentEditor.hasTextFocus(); + const oldAndNewBothEmpty = !this._commentThread.comments?.length && !commentThread.comments?.length; if (!this._reviewThreadReplyButton) { this.createReplyButton(this.commentEditor, this.form); } - if (this._commentThread.comments && this._commentThread.comments.length === 0) { + if (this._commentThread.comments && this._commentThread.comments.length === 0 && !oldAndNewBothEmpty) { this.expandReplyArea(); } @@ -169,7 +178,7 @@ export class CommentReply extends Disposable { public focusIfNeeded() { if (!this._commentThread.comments || !this._commentThread.comments.length) { this.commentEditor.focus(); - } else if (this.commentEditor.getModel()!.getValueLength() > 0) { + } else if ((this.commentEditor.getModel()?.getValueLength() ?? 0) > 0) { this.expandReplyArea(); } } @@ -187,10 +196,6 @@ export class CommentReply extends Disposable { return this.commentEditor.hasWidgetFocus(); } - public getCommentModel() { - return this.commentEditor.getModel()!; - } - public updateCanReply() { if (!this._commentThread.canReply) { this.form.style.display = 'none'; @@ -205,32 +210,12 @@ export class CommentReply extends Disposable { } setCommentEditorDecorations() { - const model = this.commentEditor.getModel(); - if (model) { - const valueLength = model.getValueLength(); - const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; - const placeholder = valueLength > 0 - ? '' - : hasExistingComments - ? (this._commentOptions?.placeHolder || nls.localize('reply', "Reply...")) - : (this._commentOptions?.placeHolder || nls.localize('newComment', "Type a new comment")); - const decorations = [{ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: placeholder, - color: `${resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4)}` - } - } - }]; + const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; + const placeholder = hasExistingComments + ? (this._commentOptions?.placeHolder || nls.localize('reply', "Reply...")) + : (this._commentOptions?.placeHolder || nls.localize('newComment', "Type a new comment")); - this.commentEditor.setDecorationsByType('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); - } + this.commentEditor.updateOptions({ placeholder }); } private createTextModelListener(commentEditor: ICodeEditor, commentForm: HTMLElement) { @@ -347,7 +332,10 @@ export class CommentReply extends Disposable { } private hideReplyArea() { - this.commentEditor.getDomNode()!.style.outline = ''; + const domNode = this.commentEditor.getDomNode(); + if (domNode) { + domNode.style.outline = ''; + } this.commentEditor.setValue(''); this._pendingComment = ''; this.form.classList.remove('expand'); @@ -357,7 +345,7 @@ export class CommentReply extends Disposable { private createReplyButton(commentEditor: ICodeEditor, commentForm: HTMLElement) { this._reviewThreadReplyButton = dom.append(commentForm, dom.$(`button.review-thread-reply-button.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); - this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._reviewThreadReplyButton, this._commentOptions?.prompt || nls.localize('reply', "Reply..."))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this._reviewThreadReplyButton, this._commentOptions?.prompt || nls.localize('reply', "Reply..."))); this._reviewThreadReplyButton.textContent = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); // bind click/escape actions for reviewThreadReplyButton and textArea diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index accc000bdce..1d20812ebe5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -30,7 +30,7 @@ interface IResourceCommentThreadEvent { commentInfos: ICommentInfo[]; } -export interface ICommentInfo extends CommentInfo { +export interface ICommentInfo extends CommentInfo { uniqueOwner: string; label?: string; } @@ -63,11 +63,11 @@ export interface ICommentController { options?: CommentOptions; contextValue?: string; owner: string; - createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise; + createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined, editorId?: string): Promise; updateCommentThreadTemplate(threadHandle: number, range: IRange): Promise; deleteCommentThreadMain(commentThreadId: string): void; toggleReaction(uri: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise; - getDocumentComments(resource: URI, token: CancellationToken): Promise; + getDocumentComments(resource: URI, token: CancellationToken): Promise>; getNotebookComments(resource: URI, token: CancellationToken): Promise; setActiveCommentAndThread(commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; } @@ -97,7 +97,7 @@ export interface ICommentService { registerCommentController(uniqueOwner: string, commentControl: ICommentController): void; unregisterCommentController(uniqueOwner?: string): void; getCommentController(uniqueOwner: string): ICommentController | undefined; - createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise; + createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined, editorId?: string): Promise; updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range): Promise; getCommentMenus(uniqueOwner: string): CommentMenus; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; @@ -361,14 +361,14 @@ export class CommentService extends Disposable implements ICommentService { return this._commentControls.get(uniqueOwner); } - async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise { + async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined, editorId?: string): Promise { const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; } - return commentController.createCommentThreadTemplate(resource, range); + return commentController.createCommentThreadTemplate(resource, range, editorId); } async updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index 62656e6f97a..bc62a581bf8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; import * as languages from 'vs/editor/common/languages'; import { Emitter } from 'vs/base/common/event'; import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; @@ -30,7 +30,7 @@ export class CommentThreadBody extends D private _onDidResize = new Emitter(); onDidResize = this._onDidResize.event; - private _commentDisposable = new Map, IDisposable>(); + private _commentDisposable = new DisposableMap, DisposableStore>(); private _markdownRenderer: MarkdownRenderer; get length() { @@ -70,7 +70,13 @@ export class CommentThreadBody extends D this._commentsElement.focus(); } - display() { + ensureFocusIntoNewEditingComment() { + if (this._commentElements.length === 1 && this._commentElements[0].isEditing) { + this._commentElements[0].setFocus(true); + } + } + + async display() { this._commentsElement = dom.append(this.container, dom.$('div.comments-container')); this._commentsElement.setAttribute('role', 'presentation'); this._commentsElement.tabIndex = 0; @@ -90,6 +96,7 @@ export class CommentThreadBody extends D } })); + this._commentDisposable.clearAndDisposeAll(); this._commentElements = []; if (this._commentThread.comments) { for (const comment of this._commentThread.comments) { @@ -98,7 +105,7 @@ export class CommentThreadBody extends D this._commentElements.push(newCommentNode); this._commentsElement.appendChild(newCommentNode.domNode); if (comment.mode === languages.CommentMode.Editing) { - newCommentNode.switchToEditMode(); + await newCommentNode.switchToEditMode(); } } } @@ -156,7 +163,7 @@ export class CommentThreadBody extends D return; } - updateCommentThread(commentThread: languages.CommentThread, preserveFocus: boolean) { + async updateCommentThread(commentThread: languages.CommentThread, preserveFocus: boolean) { const oldCommentsLen = this._commentElements.length; const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; @@ -177,11 +184,10 @@ export class CommentThreadBody extends D // del removed elements for (let i = commentElementsToDel.length - 1; i >= 0; i--) { const commentToDelete = commentElementsToDel[i]; - this._commentDisposable.get(commentToDelete)?.dispose(); - this._commentDisposable.delete(commentToDelete); + this._commentDisposable.deleteAndDispose(commentToDelete); this._commentElements.splice(commentElementsToDelIndex[i], 1); - this._commentsElement.removeChild(commentToDelete.domNode); + commentToDelete.domNode.remove(); } @@ -207,7 +213,7 @@ export class CommentThreadBody extends D } if (currentComment.mode === languages.CommentMode.Editing) { - newElement.switchToEditMode(); + await newElement.switchToEditMode(); newCommentsInEditMode.push(newElement); } } @@ -267,10 +273,12 @@ export class CommentThreadBody extends D this._parentCommentThreadWidget, this._markdownRenderer) as unknown as CommentNode; - this._register(newCommentNode); - this._commentDisposable.set(newCommentNode, newCommentNode.onDidClick(clickedNode => + const disposables: DisposableStore = new DisposableStore(); + disposables.add(newCommentNode.onDidClick(clickedNode => this._setFocusedComment(this._commentElements.findIndex(commentNode => commentNode.comment.uniqueIdInThread === clickedNode.comment.uniqueIdInThread)) )); + disposables.add(newCommentNode); + this._commentDisposable.set(newCommentNode, disposables); return newCommentNode; } @@ -283,6 +291,6 @@ export class CommentThreadBody extends D this._resizeObserver = null; } - this._commentDisposable.forEach(v => v.dispose()); + this._commentDisposable.dispose(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 9784625cd2f..d8bcd057107 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, ActionRunner } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import * as languages from 'vs/editor/common/languages'; import { IRange } from 'vs/editor/common/core/range'; @@ -26,7 +26,11 @@ import { MarshalledCommentThread } from 'vs/workbench/common/comments'; const collapseIcon = registerIcon('review-comment-collapse', Codicon.chevronUp, nls.localize('collapseIcon', 'Icon to collapse a review comment.')); const COLLAPSE_ACTION_CLASS = 'expand-review-action ' + ThemeIcon.asClassName(collapseIcon); +const DELETE_ACTION_CLASS = 'expand-review-action ' + ThemeIcon.asClassName(Codicon.trashcan); +function threadHasComments(comments: ReadonlyArray | undefined): comments is ReadonlyArray { + return !!comments && comments.length > 0; +} export class CommentThreadHeader extends Disposable { private _headElement: HTMLElement; @@ -46,6 +50,7 @@ export class CommentThreadHeader extends Disposable { super(); this._headElement = dom.$('.head'); container.appendChild(this._headElement); + this._register(toDisposable(() => this._headElement.remove())); this._fillHead(); } @@ -62,9 +67,20 @@ export class CommentThreadHeader extends Disposable { this._register(this._actionbarWidget); - this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this._delegate.collapse()); + const collapseClass = threadHasComments(this._commentThread.comments) ? COLLAPSE_ACTION_CLASS : DELETE_ACTION_CLASS; + this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), collapseClass, true, () => this._delegate.collapse()); + if (!threadHasComments(this._commentThread.comments)) { + const commentsChanged: MutableDisposable = this._register(new MutableDisposable()); + commentsChanged.value = this._commentThread.onDidChangeComments(() => { + if (threadHasComments(this._commentThread.comments)) { + this._collapseAction.class = COLLAPSE_ACTION_CLASS; + commentsChanged.clear(); + } + }); + } const menu = this._commentMenus.getCommentThreadTitleActions(this._contextKeyService); + this._register(menu); this.setActionBarActions(menu); this._register(menu); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index e09cd4f5167..4129171b360 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/review'; import * as dom from 'vs/base/browser/dom'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as languages from 'vs/editor/common/languages'; import { IMarkdownRendererOptions } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -88,7 +88,7 @@ export class CommentThreadWidget extends this._commentMenus = this.commentService.getCommentMenus(this._owner); - this._header = new CommentThreadHeader( + this._register(this._header = new CommentThreadHeader( container, { collapse: this.collapse.bind(this) @@ -98,15 +98,17 @@ export class CommentThreadWidget extends this._contextKeyService, this._scopedInstantiationService, contextMenuService - ); + )); this._header.updateCommentThread(this._commentThread); const bodyElement = dom.$('.body'); container.appendChild(bodyElement); + this._register(toDisposable(() => bodyElement.remove())); const tracker = this._register(dom.trackFocus(bodyElement)); this._register(registerNavigableContainer({ + name: 'commentThreadWidget', focusNotifiers: [tracker], focusNextWidget: () => { if (!this._commentReply?.isCommentEditorFocused()) { @@ -204,7 +206,7 @@ export class CommentThreadWidget extends }, true)); } - updateCommentThread(commentThread: languages.CommentThread) { + async updateCommentThread(commentThread: languages.CommentThread) { const shouldCollapse = (this._commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded) && (this._commentThreadState === languages.CommentThreadState.Unresolved) && (commentThread.state === languages.CommentThreadState.Resolved); this._commentThreadState = commentThread.state; @@ -213,7 +215,7 @@ export class CommentThreadWidget extends this._commentThreadDisposables = []; this._bindCommentThreadListeners(); - this._body.updateCommentThread(commentThread, this._commentReply?.isCommentEditorFocused() ?? false); + await this._body.updateCommentThread(commentThread, this._commentReply?.isCommentEditorFocused() ?? false); this._threadIsEmpty.set(!this._body.length); this._header.updateCommentThread(commentThread); this._commentReply?.updateCommentThread(commentThread); @@ -229,15 +231,15 @@ export class CommentThreadWidget extends } } - display(lineHeight: number) { + async display(lineHeight: number, focus: boolean) { const headHeight = Math.max(23, Math.ceil(lineHeight * 1.2)); // 23 is the value of `Math.ceil(lineHeight * 1.2)` with the default editor font size this._header.updateHeight(headHeight); - this._body.display(); + await this._body.display(); // create comment thread only when it supports reply if (this._commentThread.canReply) { - this._createCommentForm(); + this._createCommentForm(focus); } this._createAdditionalActions(); @@ -271,7 +273,7 @@ export class CommentThreadWidget extends this._commentReply.updateCanReply(); } else { if (this._commentThread.canReply) { - this._createCommentForm(); + this._createCommentForm(false); } } })); @@ -285,7 +287,7 @@ export class CommentThreadWidget extends })); } - private _createCommentForm() { + private _createCommentForm(focus: boolean) { this._commentReply = this._scopedInstantiationService.createInstance( CommentReply, this._owner, @@ -298,6 +300,7 @@ export class CommentThreadWidget extends this._commentOptions, this._pendingComment, this, + focus, this._containerDelegate.actionRunner ); @@ -350,6 +353,10 @@ export class CommentThreadWidget extends } } + ensureFocusIntoNewEditingComment() { + this._body.ensureFocusIntoNewEditingComment(); + } + focusCommentEditor() { this._commentReply?.expandReplyAreaAndFocusCommentEditor(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 4696f822eca..5fcfb5a4ef6 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -139,9 +139,9 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget super(editor, { keepEditorSelection: true, isAccessible: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); - this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection( + this._scopedInstantiationService = this._globalToDispose.add(instantiationService.createChild(new ServiceCollection( [IContextKeyService, this._contextKeyService] - )); + ))); const controller = this.commentService.getCommentController(this._uniqueOwner); if (controller) { @@ -150,7 +150,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._initialCollapsibleState = _pendingComment ? languages.CommentThreadCollapsibleState.Expanded : _commentThread.initialCollapsibleState; _commentThread.initialCollapsibleState = this._initialCollapsibleState; - this._isExpanded = this._initialCollapsibleState === languages.CommentThreadCollapsibleState.Expanded; this._commentThreadDisposables = []; this.create(); @@ -188,34 +187,18 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } public reveal(commentUniqueId?: number, focus: CommentWidgetFocus = CommentWidgetFocus.None) { + this.makeVisible(commentUniqueId, focus); + const comment = this._commentThread.comments?.find(comment => comment.uniqueIdInThread === commentUniqueId); + this.commentService.setActiveCommentAndThread(this.uniqueOwner, { thread: this._commentThread, comment }); + } + + private _expandAndShowZoneWidget() { if (!this._isExpanded) { this.show(this.arrowPosition(this._commentThread.range), 2); } + } - if (commentUniqueId !== undefined) { - const height = this.editor.getLayoutInfo().height; - const coords = this._commentThreadWidget.getCommentCoords(commentUniqueId); - if (coords) { - let scrollTop: number = 1; - if (this._commentThread.range) { - const commentThreadCoords = coords.thread; - const commentCoords = coords.comment; - scrollTop = this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top; - } - this.editor.setScrollTop(scrollTop); - if (focus === CommentWidgetFocus.Widget) { - this._commentThreadWidget.focus(); - } else if (focus === CommentWidgetFocus.Editor) { - this._commentThreadWidget.focusCommentEditor(); - } - return; - } - } - const rangeToReveal = this._commentThread.range - ? new Range(this._commentThread.range.startLineNumber, this._commentThread.range.startColumn, this._commentThread.range.endLineNumber + 1, 1) - : new Range(1, 1, 1, 1); - - this.editor.revealRangeInCenter(rangeToReveal); + private _setFocus(focus: CommentWidgetFocus) { if (focus === CommentWidgetFocus.Widget) { this._commentThreadWidget.focus(); } else if (focus === CommentWidgetFocus.Editor) { @@ -223,6 +206,41 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } + private _goToComment(commentUniqueId: number, focus: CommentWidgetFocus) { + const height = this.editor.getLayoutInfo().height; + const coords = this._commentThreadWidget.getCommentCoords(commentUniqueId); + if (coords) { + let scrollTop: number = 1; + if (this._commentThread.range) { + const commentThreadCoords = coords.thread; + const commentCoords = coords.comment; + scrollTop = this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top; + } + this.editor.setScrollTop(scrollTop); + this._setFocus(focus); + } else { + this._goToThread(focus); + } + } + + private _goToThread(focus: CommentWidgetFocus) { + const rangeToReveal = this._commentThread.range + ? new Range(this._commentThread.range.startLineNumber, this._commentThread.range.startColumn, this._commentThread.range.endLineNumber + 1, 1) + : new Range(1, 1, 1, 1); + + this.editor.revealRangeInCenter(rangeToReveal); + this._setFocus(focus); + } + + public makeVisible(commentUniqueId?: number, focus: CommentWidgetFocus = CommentWidgetFocus.None) { + this._expandAndShowZoneWidget(); + + if (commentUniqueId !== undefined) { + this._goToComment(commentUniqueId, focus); + } + this._goToThread(focus); + } + public getPendingComments(): { newComment: string | undefined; edits: { [key: number]: string } } { return { newComment: this._commentThreadWidget.getPendingComment(), @@ -301,8 +319,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; } - public expand() { + public expand(setActive?: boolean) { this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; + if (setActive) { + this.commentService.setActiveCommentAndThread(this.uniqueOwner, { thread: this._commentThread }); + } } public getGlyphPosition(): number { @@ -312,14 +333,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return 0; } - toggleExpand() { - if (this._isExpanded) { - this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; - } else { - this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; - } - } - async update(commentThread: languages.CommentThread) { if (this._commentThread !== commentThread) { this._commentThreadDisposables.forEach(disposable => disposable.dispose()); @@ -328,7 +341,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.bindCommentThreadListeners(); } - this._commentThreadWidget.updateCommentThread(commentThread); + await this._commentThreadWidget.updateCommentThread(commentThread); // Move comment glyph widget and show position if the line has changed. const lineNumber = this._commentThread.range?.endLineNumber ?? 1; @@ -356,13 +369,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadWidget.layout(widthInPixel); } - display(range: IRange | undefined) { + async display(range: IRange | undefined, shouldReveal: boolean) { if (range) { this._commentGlyph = new CommentGlyphWidget(this.editor, range?.endLineNumber ?? -1); this._commentGlyph.setThreadState(this._commentThread.state); } - this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight)); + await this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight), shouldReveal); this._disposables.add(this._commentThreadWidget.onDidResize(dimension => { this._refresh(dimension); })); @@ -371,8 +384,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } // If this is a new comment thread awaiting user input then we need to reveal it. - if (this._commentThread.canReply && this._commentThread.isTemplate && (!this._commentThread.comments || (this._commentThread.comments.length === 0))) { - this.reveal(); + if (shouldReveal) { + this.makeVisible(); } this.bindCommentThreadListeners(); @@ -402,6 +415,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadDisposables.push(this._commentThread.onDidChangeCollapsibleState(state => { if (state === languages.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { this.show(this.arrowPosition(this._commentThread.range), 2); + this._commentThreadWidget.ensureFocusIntoNewEditingComment(); return; } @@ -439,7 +453,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } _refresh(dimensions: dom.Dimension) { - if (dimensions.height === 0 && dimensions.width === 0) { + if ((this._isExpanded === undefined) && (dimensions.height === 0) && (dimensions.width === 0)) { this.commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; return; } @@ -463,10 +477,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._viewZone.afterLineNumber = currentPosition.lineNumber; } - if (!this._commentThread.comments || !this._commentThread.comments.length) { - this._commentThreadWidget.focusCommentEditor(); - } - const capture = StableEditorScrollState.capture(this.editor); this._relayout(computedLinesNumber); capture.restore(this.editor); diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index e57e7c315e2..4f7bfdd711d 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -25,6 +25,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; import { MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { CommentsAccessibleView } from 'vs/workbench/contrib/comments/browser/commentsAccessibleView'; +import { CommentsAccessibilityHelp } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; registerAction2(class Collapse extends ViewAction { constructor() { @@ -74,11 +79,15 @@ registerAction2(class Reply extends Action2 { id: 'comments.reply', title: nls.localize('reply', "Reply"), icon: Codicon.reply, - menu: { + precondition: ContextKeyExpr.equals('canReply', true), + menu: [{ id: MenuId.CommentsViewThreadActions, - order: 100, - when: ContextKeyExpr.equals('canReply', true) + order: 100 }, + { + id: MenuId.AccessibleView, + when: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Comments)), + }] }); } @@ -182,3 +191,6 @@ export class UnresolvedCommentsBadge extends Disposable implements IWorkbenchCon } Registry.as(Extensions.Workbench).registerWorkbenchContribution(UnresolvedCommentsBadge, LifecyclePhase.Eventually); + +AccessibleViewRegistry.register(new CommentsAccessibleView()); +AccessibleViewRegistry.register(new CommentsAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 27ba7d7e663..e9ec6e6afaa 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -3,84 +3,51 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ctxCommentEditorFocused } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import * as nls from 'vs/nls'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import * as strings from 'vs/base/common/strings'; -import { getActiveElement } from 'vs/base/browser/dom'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; +import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { Disposable } from 'vs/base/common/lifecycle'; + export namespace CommentAccessibilityHelpNLS { export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); - export const introWidget = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled ({0})."); - export const introWidgetNoKb = nls.localize('introWidgetNoKb', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus, which is currently not triggerable via keybinding."); + export const tabFocus = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus{0}.", ``); export const commentCommands = nls.localize('commentCommands', "Some useful comment commands include:"); export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); - export const nextRange = nls.localize('next', "- Go to Next Commenting Range ({0})"); - export const nextRangeNoKb = nls.localize('nextNoKb', "- Go to Next Commenting Range, which is currently not triggerable via keybinding."); - export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range ({0})"); - export const previousRangeNoKb = nls.localize('previousNoKb', "- Go to Previous Commenting Range, which is currently not triggerable via keybinding."); - export const nextCommentThreadKb = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread ({0})"); - export const nextCommentThreadNoKb = nls.localize('nextCommentThreadNoKb', "- Go to Next Comment Thread, which is currently not triggerable via keybinding."); - export const previousCommentThreadKb = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread ({0})"); - export const previousCommentThreadNoKb = nls.localize('previousCommentThreadNoKb', "- Go to Previous Comment Thread, which is currently not triggerable via keybinding."); - export const addComment = nls.localize('addComment', "- Add Comment ({0})"); - export const addCommentNoKb = nls.localize('addCommentNoKb', "- Add Comment on Current Selection, which is currently not triggerable via keybinding."); - export const submitComment = nls.localize('submitComment', "- Submit Comment ({0})"); - export const submitCommentNoKb = nls.localize('submitCommentNoKb', "- Submit Comment, accessible via tabbing, as it's currently not triggerable with a keybinding."); + export const nextRange = nls.localize('next', "- Go to Next Commenting Range{0}.", ``); + export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range{0}.", ``); + export const nextCommentThread = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread{0}.", ``); + export const previousCommentThread = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread{0}.", ``); + export const addComment = nls.localize('addCommentNoKb', "- Add Comment on Current Selection{0}.", ``); + export const submitComment = nls.localize('submitComment', "- Submit Comment{0}.", ``); } -export class CommentsAccessibilityHelpProvider implements IAccessibleContentProvider { +export class CommentsAccessibilityHelpProvider extends Disposable implements IAccessibleViewContentProvider { id = AccessibleViewProviderId.Comments; verbositySettingKey: AccessibilityVerbositySettingId = AccessibilityVerbositySettingId.Comments; options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; private _element: HTMLElement | undefined; - constructor( - @IKeybindingService private readonly _keybindingService: IKeybindingService - ) { - - } - private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string { - const kb = this._keybindingService.lookupKeybinding(commandId); - if (kb) { - return strings.format(msg, kb.getAriaLabel()); - } - return strings.format(noKbMsg, commandId); - } provideContent(): string { - this._element = getActiveElement() as HTMLElement; - const content: string[] = []; - content.push(this._descriptionForCommand(ToggleTabFocusModeAction.ID, CommentAccessibilityHelpNLS.introWidget, CommentAccessibilityHelpNLS.introWidgetNoKb) + '\n'); - content.push(CommentAccessibilityHelpNLS.commentCommands); - content.push(CommentAccessibilityHelpNLS.escape); - content.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.Submit, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.submitCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); - return content.join('\n'); + return [CommentAccessibilityHelpNLS.tabFocus, CommentAccessibilityHelpNLS.commentCommands, CommentAccessibilityHelpNLS.escape, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.previousRange].join('\n'); } onClose(): void { this._element?.focus(); } } -export class CommentsAccessibilityHelpContribution extends Disposable { - static ID: 'commentsAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(110, 'comments', accessor => { - const instantiationService = accessor.get(IInstantiationService); - const accessibleViewService = accessor.get(IAccessibleViewService); - accessibleViewService.show(instantiationService.createInstance(CommentsAccessibilityHelpProvider)); - return true; - }, ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused))); +export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 110; + readonly name = 'comments'; + readonly type = AccessibleViewType.Help; + readonly when = ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused); + getProvider(accessor: ServicesAccessor) { + return accessor.get(IInstantiationService).createInstance(CommentsAccessibilityHelpProvider); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts new file mode 100644 index 00000000000..907cd886376 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.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 { Disposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { CommentsPanel, CONTEXT_KEY_COMMENT_FOCUSED } from 'vs/workbench/contrib/comments/browser/commentsView'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; + +export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplentation { + readonly priority = 90; + readonly name = 'comment'; + readonly when = CONTEXT_KEY_COMMENT_FOCUSED; + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const contextKeyService = accessor.get(IContextKeyService); + const viewsService = accessor.get(IViewsService); + const menuService = accessor.get(IMenuService); + const commentsView = viewsService.getActiveViewWithId(COMMENTS_VIEW_ID); + const focusedCommentNode = commentsView?.focusedCommentNode; + if (!commentsView || !focusedCommentNode) { + return; + } + const menus = this._register(new CommentsMenus(menuService)); + menus.setContextKeyService(contextKeyService); + + return new CommentsAccessibleContentProvider(commentsView, focusedCommentNode, menus); + } + constructor() { + super(); + } +} + +class CommentsAccessibleContentProvider extends Disposable implements IAccessibleViewContentProvider { + constructor( + private readonly _commentsView: CommentsPanel, + private readonly _focusedCommentNode: any, + private readonly _menus: CommentsMenus, + ) { + super(); + } + 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(); + if (!commentNode || !content) { + throw new Error('Comment tree is focused but no comment is selected'); + } + return content; + } + onClose(): void { + this._commentsView.focus(); + } + provideNextContent(): string | undefined { + this._commentsView.focusNextNode(); + return this.provideContent(); + } + providePreviousContent(): string | undefined { + this._commentsView.focusPreviousNode(); + return this.provideContent(); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index b471cf60732..34d258d02c7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -46,7 +46,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { threadHasMeaningfulComments } from 'vs/workbench/contrib/comments/browser/commentsModel'; export const ID = 'editor.contrib.review'; @@ -408,7 +408,7 @@ export function revealCommentThread(commentService: ICommentService, editorServi preserveFocus: preserveFocus, selection: range ?? new Range(1, 1, 1, 1) } - } as ITextResourceEditorInput, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { if (editor) { const control = editor.getControl(); if (threadToReveal && isCodeEditor(control)) { @@ -431,6 +431,7 @@ export class CommentController implements IEditorContribution { private _commentingRangeSpaceReserved = false; private _commentingRangeAmountReserved = 0; private _computePromise: CancelablePromise> | null; + private _computeAndSetPromise: Promise | undefined; private _addInProgress!: boolean; private _emptyThreadsToAddQueue: [Range | undefined, IEditorMouseEvent | undefined][] = []; private _computeCommentingRangePromise!: CancelablePromise | null; @@ -495,10 +496,10 @@ export class CommentController implements IEditorContribution { this.globalToDispose.add(this.commentService.onDidSetDataProvider(_ => this.beginComputeAndHandleEditorChange())); this.globalToDispose.add(this.commentService.onDidUpdateCommentingRanges(_ => this.beginComputeAndHandleEditorChange())); - this.globalToDispose.add(this.commentService.onDidSetResourceCommentInfos(e => { + this.globalToDispose.add(this.commentService.onDidSetResourceCommentInfos(async e => { const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri; if (editorURI && editorURI.toString() === e.resource.toString()) { - this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null)); + await this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null)); } })); @@ -646,10 +647,12 @@ export class CommentController implements IEditorContribution { return Promise.resolve([]); }); - return this._computePromise.then(commentInfos => { - this.setComments(coalesce(commentInfos)); + this._computeAndSetPromise = this._computePromise.then(async commentInfos => { + await this.setComments(coalesce(commentInfos)); this._computePromise = null; }, error => console.log(error)); + this._computePromise.then(() => this._computeAndSetPromise = undefined); + return this._computeAndSetPromise; } private beginComputeCommentingRanges() { @@ -688,8 +691,8 @@ export class CommentController implements IEditorContribution { if (commentThreadWidget.length === 1) { commentThreadWidget[0].reveal(commentUniqueId, focus); } else if (fetchOnceIfNotExist) { - if (this._computePromise) { - this._computePromise.then(_ => { + if (this._computeAndSetPromise) { + this._computeAndSetPromise.then(_ => { this.revealCommentThread(threadId, commentUniqueId, false, focus); }); } else { @@ -729,7 +732,7 @@ export class CommentController implements IEditorContribution { return; } - const after = this.editor.getSelection().getEndPosition(); + const after = reverse ? this.editor.getSelection().getStartPosition() : this.editor.getSelection().getEndPosition(); const sortedWidgets = this._commentWidgets.sort((a, b) => { if (reverse) { const temp = a; @@ -839,6 +842,40 @@ export class CommentController implements IEditorContribution { } } + private async handleCommentAdded(editorId: string | undefined, uniqueOwner: string, thread: languages.AddedCommentThread): Promise { + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + return; + } + + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + + if (matchedNewCommentThreadZones.length) { + matchedNewCommentThreadZones[0].update(thread); + return; + } + + const continueOnCommentIndex = this._inProcessContinueOnComments.get(uniqueOwner)?.findIndex(pending => { + if (pending.range === undefined) { + return thread.range === undefined; + } else { + return Range.lift(pending.range).equalsRange(thread.range); + } + }); + let continueOnCommentText: string | undefined; + if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { + continueOnCommentText = this._inProcessContinueOnComments.get(uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; + } + + const pendingCommentText = (this._pendingNewCommentCache[uniqueOwner] && this._pendingNewCommentCache[uniqueOwner][thread.threadId]) + ?? continueOnCommentText; + const pendingEdits = this._pendingEditsCache[uniqueOwner] && this._pendingEditsCache[uniqueOwner][thread.threadId]; + const shouldReveal = thread.canReply && thread.isTemplate && (!thread.comments || (thread.comments.length === 0)) && (!thread.editorId || (thread.editorId === editorId)); + await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits); + this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread); + this.tryUpdateReservedSpace(); + } + public onModelChanged(): void { this.localToDispose.clear(); this.tryUpdateReservedSpace(); @@ -904,45 +941,17 @@ export class CommentController implements IEditorContribution { } }); - changed.forEach(thread => { + for (const thread of changed) { const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { const matchedZone = matchedZones[0]; matchedZone.update(thread); this.openCommentsView(thread); } - }); + } + const editorId = this.editor?.getId(); for (const thread of added) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); - if (matchedZones.length) { - return; - } - - const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); - - if (matchedNewCommentThreadZones.length) { - matchedNewCommentThreadZones[0].update(thread); - return; - } - - const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.uniqueOwner)?.findIndex(pending => { - if (pending.range === undefined) { - return thread.range === undefined; - } else { - return Range.lift(pending.range).equalsRange(thread.range); - } - }); - let continueOnCommentText: string | undefined; - if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { - continueOnCommentText = this._inProcessContinueOnComments.get(e.uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; - } - - const pendingCommentText = (this._pendingNewCommentCache[e.uniqueOwner] && this._pendingNewCommentCache[e.uniqueOwner][thread.threadId]) - ?? continueOnCommentText; - const pendingEdits = this._pendingEditsCache[e.uniqueOwner] && this._pendingEditsCache[e.uniqueOwner][thread.threadId]; - this.displayCommentThread(e.uniqueOwner, thread, pendingCommentText, pendingEdits); - this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads.push(thread); - this.tryUpdateReservedSpace(); + await this.handleCommentAdded(editorId, e.uniqueOwner, thread); } for (const thread of pending) { @@ -1007,7 +1016,7 @@ export class CommentController implements IEditorContribution { } private async openCommentsView(thread: languages.CommentThread) { - if (thread.comments && (thread.comments.length > 0)) { + if (thread.comments && (thread.comments.length > 0) && threadHasMeaningfulComments(thread)) { const openViewState = this.configurationService.getValue(COMMENTS_SECTION).openView; if (openViewState === 'file') { return this.viewsService.openView(COMMENTS_VIEW_ID); @@ -1021,7 +1030,7 @@ export class CommentController implements IEditorContribution { return undefined; } - private displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { + private async displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, shouldReveal: boolean, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): Promise { const editor = this.editor?.getModel(); if (!editor) { return; @@ -1035,7 +1044,7 @@ export class CommentController implements IEditorContribution { continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true }); } const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); - zoneWidget.display(thread.range); + await zoneWidget.display(thread.range, shouldReveal); this._commentWidgets.push(zoneWidget); this.openCommentsView(thread); } @@ -1089,7 +1098,7 @@ export class CommentController implements IEditorContribution { const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === (commentRange ? commentRange.endLineNumber : 0)); if (existingCommentsAtLine.length) { const allExpanded = existingCommentsAtLine.every(widget => widget.expanded); - existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse() : widget => widget.expand()); + existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse() : widget => widget.expand(true)); this.processNextThreadToAdd(); return; } else { @@ -1123,7 +1132,7 @@ export class CommentController implements IEditorContribution { if (!newCommentInfos.length || !this.editor?.hasModel()) { this._addInProgress = false; if (!newCommentInfos.length) { - throw new Error('There are no commenting ranges at the current position.'); + throw new Error(`There are no commenting ranges at the current position (${range ? 'with range' : 'without range'}).`); } return Promise.resolve(); } @@ -1169,10 +1178,10 @@ export class CommentController implements IEditorContribution { const picks: QuickPickInput[] = commentInfos.map((commentInfo) => { const { ownerId, extensionId, label } = commentInfo.action; - return { - label: label || extensionId, + return { + label: label ?? extensionId ?? ownerId, id: ownerId - }; + } satisfies IQuickPickItem; }); return picks; @@ -1203,7 +1212,7 @@ export class CommentController implements IEditorContribution { if (!this.editor) { return; } - this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range); + this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range, this.editor.getId()); this.processNextThreadToAdd(); return; } @@ -1296,7 +1305,7 @@ export class CommentController implements IEditorContribution { } } - private setComments(commentInfos: ICommentInfo[]): void { + private async setComments(commentInfos: ICommentInfo[]): Promise { if (!this.editor || !this.commentService.isCommentingEnabled) { return; } @@ -1307,7 +1316,7 @@ export class CommentController implements IEditorContribution { this.removeCommentWidgetsAndStoreCache(); let hasCommentingRanges = false; - this._commentInfos.forEach(info => { + for (const info of this._commentInfos) { if (!hasCommentingRanges && (info.commentingRanges.ranges.length > 0 || info.commentingRanges.fileComments)) { hasCommentingRanges = true; } @@ -1315,7 +1324,7 @@ export class CommentController implements IEditorContribution { const providerCacheStore = this._pendingNewCommentCache[info.uniqueOwner]; const providerEditsCacheStore = this._pendingEditsCache[info.uniqueOwner]; info.threads = info.threads.filter(thread => !thread.isDisposed); - info.threads.forEach(thread => { + for (const thread of info.threads) { let pendingComment: string | undefined = undefined; if (providerCacheStore) { pendingComment = providerCacheStore[thread.threadId]; @@ -1326,12 +1335,12 @@ export class CommentController implements IEditorContribution { pendingEdits = providerEditsCacheStore[thread.threadId]; } - this.displayCommentThread(info.uniqueOwner, thread, pendingComment, pendingEdits); - }); + await this.displayCommentThread(info.uniqueOwner, thread, false, pendingComment, pendingEdits); + } for (const thread of info.pendingCommentThreads ?? []) { this.resumePendingComment(this.editor!.getModel()!.uri, thread); } - }); + } this._commentingRangeDecorator.update(this.editor, this._commentInfos); this._commentThreadRangeDecorator.update(this.editor, this._commentInfos); diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 32d064d2800..896d352efe9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -23,10 +23,14 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { accessibilityHelpIsShown, accessibleViewCurrentProviderId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibilityHelpIsShown, accessibleViewCurrentProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; +import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { CommentsInputContentProvider } from 'vs/workbench/contrib/comments/browser/commentsInputContentProvider'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender); +registerWorkbenchContribution2(CommentsInputContentProvider.ID, CommentsInputContentProvider, WorkbenchPhase.BlockRestore); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CommentCommandId.NextThread, diff --git a/src/vs/workbench/contrib/comments/browser/commentsInputContentProvider.ts b/src/vs/workbench/contrib/comments/browser/commentsInputContentProvider.ts new file mode 100644 index 00000000000..7e7ee8ad821 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentsInputContentProvider.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 { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; +import { SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; + +export class CommentsInputContentProvider extends Disposable implements ITextModelContentProvider, IEditorContribution { + + public static readonly ID = 'comments.input.contentProvider'; + + constructor( + @ITextModelService textModelService: ITextModelService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @IModelService private readonly _modelService: IModelService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(Schemas.commentsInput, this)); + + this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, editor: ICodeEditor | null, _sideBySide?: boolean): Promise => { + if (!(editor instanceof SimpleCommentEditor)) { + return null; + } + + if (editor.getModel()?.uri.toString() !== input.resource.toString()) { + return null; + } + + if (input.options) { + applyTextEditorOptions(input.options, editor, ScrollType.Immediate); + } + return editor; + })); + } + + async provideTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + return existing ?? this._modelService.createModel('', this._languageService.createById('markdown'), resource); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentsModel.ts b/src/vs/workbench/contrib/comments/browser/commentsModel.ts index d0701d5f344..624db1d5235 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsModel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsModel.ts @@ -9,6 +9,12 @@ import { CommentThread } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { Disposable } from 'vs/base/common/lifecycle'; +import { isMarkdownString } from 'vs/base/common/htmlContent'; + +export function threadHasMeaningfulComments(thread: CommentThread): boolean { + return !!thread.comments && !!thread.comments.length && thread.comments.some(comment => isMarkdownString(comment.body) ? comment.body.value.length > 0 : comment.body.length > 0); + +} export interface ICommentsModel { hasCommentThreads(): boolean; @@ -38,9 +44,6 @@ export class CommentsModel extends Disposable implements ICommentsModel { return resource; }).flat(); }).flat(); - this._resourceCommentThreads.sort((a, b) => { - return a.resource.toString() > b.resource.toString() ? 1 : -1; - }); } public setCommentThreads(uniqueOwner: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { @@ -116,7 +119,14 @@ export class CommentsModel extends Disposable implements ICommentsModel { } public hasCommentThreads(): boolean { - return !!this._resourceCommentThreads.length; + // There's a resource with at least one thread + return !!this._resourceCommentThreads.length && this._resourceCommentThreads.some(resource => { + // At least one of the threads in the the resource has comments + return (resource.commentThreads.length > 0) && resource.commentThreads.some(thread => { + // At least one of the comments in the thread is not empty + return threadHasMeaningfulComments(thread.thread); + }); + }); } public getMessage(): string { diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 1c0c588d215..29100b80901 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -32,17 +32,17 @@ import { IStyleOverride } from 'vs/platform/theme/browser/defaultStyles'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { CommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MarshalledCommentThread, MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_STORAGE_ID = 'Comments'; @@ -76,7 +76,7 @@ interface ICommentThreadTemplateData { disposables: IDisposable[]; } -class CommentsModelVirualDelegate implements IListVirtualDelegate { +class CommentsModelVirtualDelegate implements IListVirtualDelegate { private static readonly RESOURCE_ID = 'resource-with-comments'; private static readonly COMMENT_ID = 'comment-node'; @@ -90,10 +90,10 @@ class CommentsModelVirualDelegate implements IListVirtualDelegate private menus: CommentsMenus, @IOpenerService private readonly openerService: IOpenerService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IHoverService private readonly hoverService: IHoverService, @IThemeService private themeService: IThemeService ) { } @@ -201,7 +201,7 @@ export class CommentNodeRenderer implements IListRenderer const threadMetadata = { icon: dom.append(metadata, dom.$('.icon')), userNames: dom.append(metadata, dom.$('.user')), - timestamp: new TimestampWidget(this.configurationService, dom.append(metadata, dom.$('.timestamp-container'))), + timestamp: new TimestampWidget(this.configurationService, this.hoverService, dom.append(metadata, dom.$('.timestamp-container'))), relevance: dom.append(metadata, dom.$('.relevance')), separator: dom.append(metadata, dom.$('.separator')), commentPreview: dom.append(metadata, dom.$('.text')), @@ -221,7 +221,7 @@ export class CommentNodeRenderer implements IListRenderer count: dom.append(snippetContainer, dom.$('.count')), lastReplyDetail: dom.append(snippetContainer, dom.$('.reply-detail')), separator: dom.append(snippetContainer, dom.$('.separator')), - timestamp: new TimestampWidget(this.configurationService, dom.append(snippetContainer, dom.$('.timestamp-container'))), + timestamp: new TimestampWidget(this.configurationService, this.hoverService, dom.append(snippetContainer, dom.$('.timestamp-container'))), }; repliesMetadata.separator.innerText = '\u00b7'; repliesMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.indent)); @@ -231,8 +231,10 @@ export class CommentNodeRenderer implements IListRenderer } private getCountString(commentCount: number): string { - if (commentCount > 1) { - return nls.localize('commentsCount', "{0} comments", commentCount); + if (commentCount > 2) { + return nls.localize('commentsCountReplies', "{0} replies", commentCount - 1); + } else if (commentCount === 2) { + return nls.localize('commentsCountReply', "1 reply"); } else { return nls.localize('commentCount', "1 comment"); } @@ -300,7 +302,7 @@ export class CommentNodeRenderer implements IListRenderer const renderedComment = this.getRenderedComment(originalComment.comment.body, disposables); templateData.disposables.push(renderedComment); templateData.threadMetadata.commentPreview.appendChild(renderedComment.element.firstElementChild ?? renderedComment.element); - templateData.disposables.push(setupCustomHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? '')); + templateData.disposables.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? '')); } if (node.element.range) { @@ -317,7 +319,7 @@ export class CommentNodeRenderer implements IListRenderer commentControlHandle: node.element.controllerHandle, commentThreadHandle: node.element.threadHandle, $mid: MarshalledId.CommentThread - } as MarshalledCommentThread; + } satisfies MarshalledCommentThread; if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; @@ -448,7 +450,7 @@ export class CommentsList extends WorkbenchObjectTree('commentsView.hasComments', false); export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey('commentsView.someCommentsExpanded', false); +export const CONTEXT_KEY_COMMENT_FOCUSED = new RawContextKey('commentsView.commentFocused', false); const VIEW_STORAGE_ID = 'commentsViewState'; -function createResourceCommentsIterator(model: ICommentsModel): Iterable> { - return Iterable.map(model.resourceCommentThreads, m => { - const CommentNodeIt = Iterable.from(m.commentThreads); - const children = Iterable.map(CommentNodeIt, r => ({ element: r })); +type CommentsTreeNode = CommentsModel | ResourceWithCommentThreads | CommentNode; - return { element: m, children }; - }); +function createResourceCommentsIterator(model: ICommentsModel): Iterable> { + const result: ITreeElement[] = []; + + for (const m of model.resourceCommentThreads) { + const children = []; + for (const r of m.commentThreads) { + if (threadHasMeaningfulComments(r.thread)) { + children.push({ element: r }); + } + } + if (children.length > 0) { + result.push({ element: m, children }); + } + } + return result; } export class CommentsPanel extends FilterViewPane implements ICommentsView { @@ -56,6 +70,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { private totalComments: number = 0; private readonly hasCommentsContextKey: IContextKey; private readonly someCommentsExpandedContextKey: IContextKey; + private readonly commentsFocusedContextKey: IContextKey; private readonly filter: Filter; readonly filters: CommentsFilters; @@ -67,6 +82,57 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; + get focusedCommentNode(): CommentNode | undefined { + const focused = this.tree?.getFocus(); + if (focused?.length === 1 && focused[0] instanceof CommentNode) { + return focused[0]; + } + return undefined; + } + + get focusedCommentInfo(): string | undefined { + if (!this.focusedCommentNode) { + return; + } + return this.getScreenReaderInfoForNode(this.focusedCommentNode); + } + + focusNextNode(): void { + if (!this.tree) { + return; + } + const focused = this.tree.getFocus()?.[0]; + if (!focused) { + return; + } + let next = this.tree.navigate(focused).next(); + while (next && !(next instanceof CommentNode)) { + next = this.tree.navigate(next).next(); + } + if (!next) { + return; + } + this.tree.setFocus([next]); + } + + focusPreviousNode(): void { + if (!this.tree) { + return; + } + const focused = this.tree.getFocus()?.[0]; + if (!focused) { + return; + } + let previous = this.tree.navigate(focused).previous(); + while (previous && !(previous instanceof CommentNode)) { + previous = this.tree.navigate(previous).previous(); + } + if (!previous) { + return; + } + this.tree.setFocus([previous]); + } + constructor( options: IViewPaneOptions, @IInstantiationService instantiationService: IInstantiationService, @@ -80,8 +146,10 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { @IThemeService themeService: IThemeService, @ICommentService private readonly commentService: ICommentService, @ITelemetryService telemetryService: ITelemetryService, + @IHoverService hoverService: IHoverService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IPathService private readonly pathService: IPathService ) { const stateMemento = new Memento(VIEW_STORAGE_ID, storageService); const viewState = stateMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -94,15 +162,17 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { text: viewState['filter'] || '', focusContextKey: CommentsViewFilterFocusContextKey.key } - }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); this.hasCommentsContextKey = CONTEXT_KEY_HAS_COMMENTS.bindTo(contextKeyService); this.someCommentsExpandedContextKey = CONTEXT_KEY_SOME_COMMENTS_EXPANDED.bindTo(contextKeyService); + this.commentsFocusedContextKey = CONTEXT_KEY_COMMENT_FOCUSED.bindTo(contextKeyService); this.stateMemento = stateMemento; this.viewState = viewState; this.filters = this._register(new CommentsFilters({ showResolved: this.viewState['showResolved'] !== false, showUnresolved: this.viewState['showUnresolved'] !== false, + sortBy: this.viewState['sortBy'], }, this.contextKeyService)); this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved)); @@ -110,6 +180,9 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { if (event.showResolved || event.showUnresolved) { this.updateFilter(); } + if (event.sortBy) { + this.refresh(); + } })); this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter())); } @@ -119,6 +192,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.viewState['filterHistory'] = this.filterWidget.getHistory(); this.viewState['showResolved'] = this.filters.showResolved; this.viewState['showUnresolved'] = this.filters.showUnresolved; + this.viewState['sortBy'] = this.filters.sortBy; this.stateMemento.saveMemento(); super.saveState(); } @@ -126,6 +200,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { override render(): void { super.render(); this._register(registerNavigableContainer({ + name: 'commentsView', focusNotifiers: [this, this.filterWidget], focusNextWidget: () => { if (this.filterWidget.hasFocus()) { @@ -211,10 +286,10 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } } - private async renderComments(): Promise { + private renderComments(): void { this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.renderMessage(); - await this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); + this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); } public collapseAll() { @@ -260,54 +335,102 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads()); } - private getAriaForNode(element: CommentNode) { + private getScreenReaderInfoForNode(element: CommentNode, forAriaLabel?: boolean): string { + let accessibleViewHint = ''; + if (forAriaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.Comments)) { + const kbLabel = this.keybindingService.lookupKeybinding(AccessibleViewAction.id)?.getAriaLabel(); + accessibleViewHint = kbLabel ? nls.localize('acessibleViewHint', "Inspect this in the accessible view ({0}).\n", kbLabel) : nls.localize('acessibleViewHintNoKbOpen', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.\n"); + } + const replyCount = this.getReplyCountAsString(element, forAriaLabel); + const replies = this.getRepliesAsString(element, forAriaLabel); if (element.range) { if (element.threadRelevance === CommentThreadApplicability.Outdated) { - return nls.localize('resourceWithCommentLabelOutdated', - "Outdated from ${0} at line {1} column {2} in {3}, source: {4}", + return accessibleViewHint + nls.localize('resourceWithCommentLabelOutdated', + "Outdated from {0} at line {1} column {2} in {3},{4} comment: {5}", element.comment.userName, element.range.startLineNumber, element.range.startColumn, basename(element.resource), + replyCount, (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); + ) + replies; } else { - return nls.localize('resourceWithCommentLabel', - "${0} at line {1} column {2} in {3}, source: {4}", + return accessibleViewHint + nls.localize('resourceWithCommentLabel', + "{0} at line {1} column {2} in {3},{4} comment: {5}", element.comment.userName, element.range.startLineNumber, element.range.startColumn, basename(element.resource), - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); + replyCount, + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value, + ) + replies; } } else { if (element.threadRelevance === CommentThreadApplicability.Outdated) { - return nls.localize('resourceWithCommentLabelFileOutdated', - "Outdated from {0} in {1}, source: {2}", + return accessibleViewHint + nls.localize('resourceWithCommentLabelFileOutdated', + "Outdated from {0} in {1},{2} comment: {3}", element.comment.userName, basename(element.resource), + replyCount, (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); + ) + replies; } else { - return nls.localize('resourceWithCommentLabelFile', - "{0} in {1}, source: {2}", + return accessibleViewHint + nls.localize('resourceWithCommentLabelFile', + "{0} in {1},{2} comment: {3}", element.comment.userName, basename(element.resource), + replyCount, (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); + ) + replies; } } } + private getRepliesAsString(node: CommentNode, forAriaLabel?: boolean): string { + if (!node.replies.length || forAriaLabel) { + return ''; + } + return '\n' + node.replies.map(reply => nls.localize('resourceWithRepliesLabel', + "{0} {1}", + reply.comment.userName, + (typeof reply.comment.body === 'string') ? reply.comment.body : reply.comment.body.value) + ).join('\n'); + } + + private getReplyCountAsString(node: CommentNode, forAriaLabel?: boolean): string { + return node.replies.length && !forAriaLabel ? nls.localize('replyCount', " {0} replies,", node.replies.length) : ''; + } + private createTree(): void { this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, { - overrideStyles: { listBackground: this.getBackgroundColor() }, + overrideStyles: this.getLocationBasedColors().listOverrideStyles, selectionNavigation: true, filter: this.filter, + sorter: { + compare: (a: CommentsTreeNode, b: CommentsTreeNode) => { + if (a instanceof CommentsModel || b instanceof CommentsModel) { + return 0; + } + if (this.filters.sortBy === CommentsSortOrder.UpdatedAtDescending) { + return a.lastUpdatedAt > b.lastUpdatedAt ? -1 : 1; + } else if (this.filters.sortBy === CommentsSortOrder.ResourceAscending) { + if (a instanceof ResourceWithCommentThreads && b instanceof ResourceWithCommentThreads) { + const workspaceScheme = this.pathService.defaultUriScheme; + if ((a.resource.scheme !== b.resource.scheme) && (a.resource.scheme === workspaceScheme || b.resource.scheme === workspaceScheme)) { + // Workspace scheme should always come first + return b.resource.scheme === workspaceScheme ? 1 : -1; + } + return a.resource.toString() > b.resource.toString() ? 1 : -1; + } else if (a instanceof CommentNode && b instanceof CommentNode && a.thread.range && b.thread.range) { + return a.thread.range?.startLineNumber > b.thread.range?.startLineNumber ? 1 : -1; + } + } + return 0; + }, + }, keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: (item: CommentsModel | ResourceWithCommentThreads | CommentNode) => { + getKeyboardNavigationLabel: (item: CommentsTreeNode) => { return undefined; } }, @@ -320,7 +443,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { return nls.localize('resourceWithCommentThreadsLabel', "Comments in {0}, full path {1}", basename(element.resource), element.resource.fsPath); } if (element instanceof CommentNode) { - return this.getAriaForNode(element); + return this.getScreenReaderInfoForNode(element, true); } return ''; }, @@ -341,6 +464,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this._register(this.tree.onDidChangeCollapseState(() => { this.updateSomeCommentsExpanded(); })); + this._register(this.tree.onDidFocus(() => this.commentsFocusedContextKey.set(true))); + this._register(this.tree.onDidBlur(() => this.commentsFocusedContextKey.set(false))); } private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void { @@ -362,11 +487,8 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } if (this.isVisible()) { this.hasCommentsContextKey.set(this.commentService.commentsModel.hasCommentThreads()); - - this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.cachedFilterStats = undefined; - this.renderMessage(); - this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel)); + this.renderComments(); if (this.tree.getSelection().length === 0 && this.commentService.commentsModel.hasCommentThreads()) { const firstComment = this.commentService.commentsModel.resourceCommentThreads[0].commentThreads[0]; diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index 7a0f4d21531..8fedca8e845 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -10,24 +10,34 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments'; -import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { viewFilterSubmenu } from 'vs/workbench/browser/parts/views/viewFilter'; +import { Codicon } from 'vs/base/common/codicons'; + +export const enum CommentsSortOrder { + ResourceAscending = 'resourceAscending', + UpdatedAtDescending = 'updatedAtDescending', +} + const CONTEXT_KEY_SHOW_RESOLVED = new RawContextKey('commentsView.showResolvedFilter', true); const CONTEXT_KEY_SHOW_UNRESOLVED = new RawContextKey('commentsView.showUnResolvedFilter', true); +const CONTEXT_KEY_SORT_BY = new RawContextKey('commentsView.sortBy', CommentsSortOrder.ResourceAscending); export interface CommentsFiltersChangeEvent { showResolved?: boolean; showUnresolved?: boolean; + sortBy?: CommentsSortOrder; } interface CommentsFiltersOptions { showResolved: boolean; showUnresolved: boolean; + sortBy: CommentsSortOrder; } export class CommentsFilters extends Disposable { @@ -39,6 +49,7 @@ export class CommentsFilters extends Disposable { super(); 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); @@ -48,7 +59,7 @@ export class CommentsFilters extends Disposable { set showUnresolved(showUnresolved: boolean) { if (this._showUnresolved.get() !== showUnresolved) { this._showUnresolved.set(showUnresolved); - this._onDidChange.fire({ showUnresolved: true }); + this._onDidChange.fire({ showUnresolved: true }); } } @@ -59,10 +70,20 @@ export class CommentsFilters extends Disposable { set showResolved(showResolved: boolean) { if (this._showResolved.get() !== showResolved) { this._showResolved.set(showResolved); - this._onDidChange.fire({ showResolved: true }); + this._onDidChange.fire({ showResolved: true }); } } + private _sortBy = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); + get sortBy(): CommentsSortOrder { + return this._sortBy.get()!; + } + set sortBy(sortBy: CommentsSortOrder) { + if (this._sortBy.get() !== sortBy) { + this._sortBy.set(sortBy); + this._onDidChange.fire({ sortBy }); + } + } } registerAction2(class extends ViewAction { @@ -168,3 +189,64 @@ registerAction2(class extends ViewAction { view.filters.showResolved = !view.filters.showResolved; } }); + +const commentSortSubmenu = new MenuId('submenu.filter.commentSort'); +MenuRegistry.appendMenuItem(viewFilterSubmenu, { + submenu: commentSortSubmenu, + title: localize('comment sorts', "Sort By"), + group: '2_sort', + icon: Codicon.history, + when: ContextKeyExpr.equals('view', COMMENTS_VIEW_ID), +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByUpdatedAt`, + title: localize('toggle sorting by updated at', "Updated Time"), + category: localize('comments', "Comments"), + icon: Codicon.history, + viewId: COMMENTS_VIEW_ID, + toggled: { + condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.UpdatedAtDescending), + title: localize('sorting by updated at', "Updated Time"), + }, + menu: { + id: commentSortSubmenu, + group: 'navigation', + order: 1, + isHiddenByDefault: false, + }, + }); + } + + async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise { + view.filters.sortBy = CommentsSortOrder.UpdatedAtDescending; + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByResource`, + title: localize('toggle sorting by resource', "File"), + category: localize('comments', "Comments"), + icon: Codicon.history, + viewId: COMMENTS_VIEW_ID, + toggled: { + condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.ResourceAscending), + title: localize('sorting by file', "File"), + }, + menu: { + id: commentSortSubmenu, + group: 'navigation', + order: 0, + isHiddenByDefault: false, + }, + }); + } + + async runInView(serviceAccessor: ServicesAccessor, view: ICommentsView): Promise { + view.filters.sortBy = 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 938c658fd2d..8527341bdae 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -90,11 +90,14 @@ .comments-panel .comments-panel-container .tree-container .comment-thread-container .text * { margin: 0; text-overflow: ellipsis; - max-width: 500px; overflow: hidden; padding-right: 5px; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata .text * { + max-width: 700px; +} + .comments-panel .comments-panel-container .tree-container .comment-thread-container .range { opacity: 0.8; } diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index cad293ca09e..f643c68547f 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -244,11 +244,6 @@ margin-top: 0; } -.review-widget .body .comment-body code { - border-radius: 3px; - padding: 0 0.4em; -} - .review-widget .body .comment-body span { white-space: pre; } @@ -386,7 +381,7 @@ margin-right: 12px; } -.review-widget .body .comment-form .monaco-text-button, +.review-widget .body .comment-form .form-actions .monaco-text-button, .review-widget .body .edit-container .monaco-text-button { width: auto; padding: 4px 10px; @@ -530,7 +525,8 @@ div.preview.inline .monaco-editor .comment-range-glyph { .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before, .monaco-editor .margin-view-overlays .comment-range-glyph.line-hover:before { - content: "\ea60"; + content: var(--vscode-icon-plus-content); + font-family: var(--vscode-icon-plus-font-family); font-family: "codicon"; border-radius: 3px; width: 18px !important; @@ -557,11 +553,13 @@ div.preview.inline .monaco-editor .comment-range-glyph { } .monaco-editor .comment-range-glyph.comment-thread:before { - content: "\ea6b"; + content: var(--vscode-icon-comment-add-content); + font-family: var(--vscode-icon-comment-add-font-family); } .monaco-editor .comment-range-glyph.comment-thread-unresolved:before { - content: "\ec0a"; + content: var(--vscode-icon-comment-unresolved-content); + font-family: var(--vscode-icon-comment-unresolved-font-family); } .monaco-editor.inline-comment .margin-view-overlays .codicon-folding-expanded, diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index b5b62904843..532fa6f5401 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -28,6 +28,15 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { clamp } from 'vs/base/common/numbers'; +import { CopyPasteController } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController'; +import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; +import { DropIntoEditorController } from 'vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController'; +import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; export const ctxCommentEditorFocused = new RawContextKey('commentEditorFocused', false); export const MIN_EDITOR_HEIGHT = 5 * 18; @@ -63,8 +72,19 @@ export class SimpleCommentEditor extends CodeEditorWidget { { id: SuggestController.ID, ctor: SuggestController, instantiation: EditorContributionInstantiation.Eager }, { id: SnippetController2.ID, ctor: SnippetController2, instantiation: EditorContributionInstantiation.Lazy }, { id: TabCompletionController.ID, ctor: TabCompletionController, instantiation: EditorContributionInstantiation.Eager }, // eager because it needs to define a context key - { id: EditorDictation.ID, ctor: EditorDictation, instantiation: EditorContributionInstantiation.Lazy } - ] + { id: EditorDictation.ID, ctor: EditorDictation, instantiation: EditorContributionInstantiation.Lazy }, + ...EditorExtensionsRegistry.getSomeEditorContributions([ + CopyPasteController.ID, + DropIntoEditorController.ID, + LinkDetector.ID, + MessageController.ID, + HoverController.ID, + SelectionClipboardContributionID, + InlineCompletionsController.ID, + CodeActionController.ID, + ]) + ], + contextMenuId: MenuId.SimpleEditorContext }; super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, scopedContextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService); @@ -113,9 +133,11 @@ export class SimpleCommentEditor extends CodeEditorWidget { minimap: { enabled: false }, + dropIntoEditor: { enabled: true }, autoClosingBrackets: configurationService.getValue('editor.autoClosingBrackets'), quickSuggestions: false, accessibilitySupport: configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'), + fontFamily: configurationService.getValue('editor.fontFamily'), }; } } @@ -123,7 +145,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { export function calculateEditorHeight(parentEditor: LayoutableEditor, editor: ICodeEditor, currentHeight: number): number { const layoutInfo = editor.getLayoutInfo(); const lineHeight = editor.getOption(EditorOption.lineHeight); - const contentHeight = (editor._getViewModel()?.getLineCount()! * lineHeight) ?? editor.getContentHeight(); // Can't just call getContentHeight() because it returns an incorrect, large, value when the editor is first created. + const contentHeight = (editor._getViewModel()?.getLineCount()! * lineHeight); // Can't just call getContentHeight() because it returns an incorrect, large, value when the editor is first created. if ((contentHeight > layoutInfo.height) || (contentHeight < layoutInfo.height && currentHeight > MIN_EDITOR_HEIGHT)) { const linesToAdd = Math.ceil((contentHeight - layoutInfo.height) / lineHeight); diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index dbfad43dfd0..583ccaa7963 100644 --- a/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { fromNow } from 'vs/base/common/date'; import { Disposable } from 'vs/base/common/lifecycle'; import { language } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import type { IHoverService } from 'vs/platform/hover/browser/hover'; import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; export class TimestampWidget extends Disposable { @@ -17,14 +18,19 @@ export class TimestampWidget extends Disposable { private _timestamp: Date | undefined; private _useRelativeTime: boolean; - private hover: ICustomHover; + private hover: IManagedHover; - constructor(private configurationService: IConfigurationService, container: HTMLElement, timeStamp?: Date) { + constructor( + private configurationService: IConfigurationService, + hoverService: IHoverService, + container: HTMLElement, + timeStamp?: Date + ) { super(); this._date = dom.append(container, dom.$('span.timestamp')); this._date.style.display = 'none'; this._useRelativeTime = this.useRelativeTimeSetting; - this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this._date, '')); + this.hover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this._date, '')); this.setTimestamp(timeStamp); } diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index fbf25f6c06b..716985b311b 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -42,6 +42,23 @@ export class CommentNode { hasReply(): boolean { return this.replies && this.replies.length !== 0; } + + private _lastUpdatedAt: string | undefined; + + get lastUpdatedAt(): string { + if (this._lastUpdatedAt === undefined) { + let updatedAt = this.comment.timestamp || ''; + if (this.replies.length) { + const reply = this.replies[this.replies.length - 1]; + const replyUpdatedAt = reply.lastUpdatedAt; + if (replyUpdatedAt > updatedAt) { + updatedAt = replyUpdatedAt; + } + } + this._lastUpdatedAt = updatedAt; + } + return this._lastUpdatedAt; + } } export class ResourceWithCommentThreads { @@ -71,5 +88,25 @@ export class ResourceWithCommentThreads { return commentNodes[0]; } + + private _lastUpdatedAt: string | undefined; + + get lastUpdatedAt() { + if (this._lastUpdatedAt === undefined) { + let updatedAt = ''; + // Return result without cahcing as we expect data to arrive later + if (!this.commentThreads.length) { + return updatedAt; + } + for (const thread of this.commentThreads) { + const threadUpdatedAt = thread.lastUpdatedAt; + if (threadUpdatedAt && threadUpdatedAt > updatedAt) { + updatedAt = threadUpdatedAt; + } + } + this._lastUpdatedAt = updatedAt; + } + return this._lastUpdatedAt; + } } diff --git a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index cd5f0ddf60c..9c7d9e80c16 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IRange, Range } from 'vs/editor/common/core/range'; import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; @@ -19,6 +19,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverService'; class TestCommentThread implements CommentThread { isDocumentCommentThread(): this is CommentThread { @@ -118,6 +120,7 @@ suite('Comments View', function () { disposables = new DisposableStore(); instantiationService = workbenchInstantiationService({}, disposables); instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IHoverService, NullHoverService); instantiationService.stub(IContextViewService, {}); instantiationService.stub(IViewDescriptorService, new TestViewDescriptorService()); commentService = instantiationService.createInstance(CommentService); diff --git a/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts b/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts index e903b24dc39..3f607d926f3 100644 --- a/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts +++ b/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts @@ -3,24 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -class ContextMenuContribution implements IWorkbenchContribution { - - private readonly disposables = new DisposableStore(); +class ContextMenuContribution extends Disposable implements IWorkbenchContribution { constructor( @ILayoutService layoutService: ILayoutService, @IContextMenuService contextMenuService: IContextMenuService ) { + super(); + const update = (visible: boolean) => layoutService.activeContainer.classList.toggle('context-menu-visible', visible); - contextMenuService.onDidShowContextMenu(() => update(true), null, this.disposables); - contextMenuService.onDidHideContextMenu(() => update(false), null, this.disposables); + this._register(contextMenuService.onDidShowContextMenu(() => update(true))); + this._register(contextMenuService.onDidHideContextMenu(() => update(false))); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 6c99db87599..e2fa81aee13 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -12,7 +12,6 @@ import { extname, isEqual } from 'vs/base/common/resources'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -24,7 +23,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { CONTEXT_ACTIVE_CUSTOM_EDITOR_ID, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; -import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupContextKeyProvider, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, IEditorType, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ContributedCustomEditors } from '../common/contributedCustomEditors'; @@ -40,16 +39,12 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _models = new CustomEditorModelManager(); - private readonly _activeCustomEditorId: IContextKey; - private readonly _focusedCustomEditorIsEditable: IContextKey; - private readonly _onDidChangeEditorTypes = this._register(new Emitter()); public readonly onDidChangeEditorTypes: Event = this._onDidChangeEditorTypes.event; private readonly _fileEditorFactory = Registry.as(EditorExtensions.EditorFactory).getFileEditorFactory(); constructor( - @IContextKeyService contextKeyService: IContextKeyService, @IFileService fileService: IFileService, @IStorageService storageService: IStorageService, @IEditorService private readonly editorService: IEditorService, @@ -60,9 +55,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ) { super(); - this._activeCustomEditorId = CONTEXT_ACTIVE_CUSTOM_EDITOR_ID.bindTo(contextKeyService); - this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); - this._contributedEditors = this._register(new ContributedCustomEditors(storageService)); // Register the contribution points only emitting one change from the resolver this.editorResolverService.bufferChangeEvents(this.registerContributionPoints.bind(this)); @@ -70,10 +62,25 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._register(this._contributedEditors.onChange(() => { // Register the contribution points only emitting one change from the resolver this.editorResolverService.bufferChangeEvents(this.registerContributionPoints.bind(this)); - this.updateContexts(); this._onDidChangeEditorTypes.fire(); })); - this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); + + // Register group context key providers. + // These set the context keys for each editor group and the global context + const activeCustomEditorContextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: CONTEXT_ACTIVE_CUSTOM_EDITOR_ID, + getGroupContextKeyValue: group => this.getActiveCustomEditorId(group), + onDidChange: this.onDidChangeEditorTypes + }; + + const customEditorIsEditableContextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, + getGroupContextKeyValue: group => this.getCustomEditorIsEditable(group), + onDidChange: this.onDidChangeEditorTypes + }; + + this._register(this.editorGroupService.registerContextKeyProvider(activeCustomEditorContextKeyProvider)); + this._register(this.editorGroupService.registerContextKeyProvider(customEditorIsEditableContextKeyProvider)); this._register(fileService.onDidRunOperation(e => { if (e.isOperation(FileOperation.MOVE)) { @@ -88,8 +95,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._register(RedoCommand.addImplementation(PRIORITY, 'custom-editor', () => { return this.withActiveCustomEditor(editor => editor.redo()); })); - - this.updateContexts(); } getEditorTypes(): IEditorType[] { @@ -127,7 +132,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ priority: contributedEditor.priority, }, { - singlePerResource: () => !this.getCustomEditorCapabilities(contributedEditor.id)?.supportsMultipleEditorsPerDocument ?? true + singlePerResource: () => !(this.getCustomEditorCapabilities(contributedEditor.id)?.supportsMultipleEditorsPerDocument ?? false) }, { createEditorInput: ({ resource }, group) => { @@ -193,17 +198,24 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this._editorCapabilities.get(viewType); } - private updateContexts() { - const activeEditorPane = this.editorService.activeEditorPane; + private getActiveCustomEditorId(group: IEditorGroup): string { + const activeEditorPane = group.activeEditorPane; const resource = activeEditorPane?.input?.resource; if (!resource) { - this._activeCustomEditorId.reset(); - this._focusedCustomEditorIsEditable.reset(); - return; + return ''; } - this._activeCustomEditorId.set(activeEditorPane?.input instanceof CustomEditorInput ? activeEditorPane.input.viewType : ''); - this._focusedCustomEditorIsEditable.set(activeEditorPane?.input instanceof CustomEditorInput); + return activeEditorPane?.input instanceof CustomEditorInput ? activeEditorPane.input.viewType : ''; + } + + private getCustomEditorIsEditable(group: IEditorGroup): boolean { + const activeEditorPane = group.activeEditorPane; + const resource = activeEditorPane?.input?.resource; + if (!resource) { + return false; + } + + return activeEditorPane?.input instanceof CustomEditorInput; } private async handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): Promise { diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index fe763dc159a..0515cc780f3 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -8,7 +8,6 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { Codicon } from 'vs/base/common/codicons'; @@ -18,8 +17,11 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { IDebugService, IExpression, IExpressionValue } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, ExpressionContainer, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; @@ -34,7 +36,12 @@ const $ = dom.$; export interface IRenderValueOptions { showChanged?: boolean; maxValueLength?: number; - showHover?: boolean; + /** If set, a hover will be shown on the element. Requires a disposable store for usage. */ + hover?: DisposableStore | { + store: DisposableStore; + commands: { id: string; args: unknown[] }[]; + commandService: ICommandService; + }; colorize?: boolean; linkDetector?: LinkDetector; } @@ -42,6 +49,7 @@ export interface IRenderValueOptions { export interface IVariableTemplateData { expression: HTMLElement; name: HTMLElement; + type: HTMLElement; value: HTMLElement; label: HighlightedLabel; lazyButton: HTMLElement; @@ -54,7 +62,7 @@ export function renderViewTree(container: HTMLElement): HTMLElement { return treeContainer; } -export function renderExpressionValue(expressionOrValue: IExpressionValue | string, container: HTMLElement, options: IRenderValueOptions): void { +export function renderExpressionValue(expressionOrValue: IExpressionValue | string, container: HTMLElement, options: IRenderValueOptions, hoverService: IHoverService): void { let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value; // remove stale classes @@ -99,18 +107,44 @@ export function renderExpressionValue(expressionOrValue: IExpressionValue | stri } else { container.textContent = value; } - if (options.showHover) { - container.title = value || ''; + + if (options.hover) { + const { store, commands, commandService } = options.hover instanceof DisposableStore ? { store: options.hover, commands: [], commandService: undefined } : options.hover; + store.add(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => { + const container = dom.$('div'); + const markdownHoverElement = dom.$('div.hover-row'); + const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); + const hoverContentsPre = dom.append(hoverContentsElement, dom.$('pre.debug-var-hover-pre')); + hoverContentsPre.textContent = value; + container.appendChild(markdownHoverElement); + return container; + }, { + actions: commands.map(({ id, args }) => { + const description = CommandsRegistry.getCommand(id)?.metadata?.description; + return { + label: typeof description === 'string' ? description : description ? description.value : id, + commandId: id, + run: () => commandService!.executeCommand(id, ...args), + }; + }) + })); } } -export function renderVariable(variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector): void { +export function renderVariable(store: DisposableStore, commandService: ICommandService, hoverService: IHoverService, variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector, displayType?: boolean): void { if (variable.available) { + data.type.textContent = ''; let text = variable.name; if (variable.value && typeof variable.name === 'string') { - text += ':'; + if (variable.type && displayType) { + text += ': '; + data.type.textContent = variable.type + ' ='; + } else { + text += ' ='; + } } - data.label.set(text, highlights, variable.type ? variable.type : variable.name); + + data.label.set(text, highlights, variable.type && !displayType ? variable.type : variable.name); data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); } else if (variable.value && typeof variable.name === 'string' && variable.name) { @@ -118,13 +152,20 @@ export function renderVariable(variable: Variable, data: IVariableTemplateData, } data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy); + const commands = [ + { id: COPY_VALUE_ID, args: [variable, [variable]] as unknown[] } + ]; + if (variable.evaluateName) { + commands.push({ id: COPY_EVALUATE_PATH_ID, args: [{ variable }] }); + } + renderExpressionValue(variable, data.value, { showChanged, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - showHover: true, + hover: { store, commands, commandService }, colorize: true, linkDetector - }); + }, hoverService); } export interface IInputBoxOptions { @@ -138,6 +179,7 @@ export interface IInputBoxOptions { export interface IExpressionTemplateData { expression: HTMLElement; name: HTMLSpanElement; + type: HTMLSpanElement; value: HTMLSpanElement; inputBoxContainer: HTMLElement; actionBar?: ActionBar; @@ -184,6 +226,7 @@ export abstract class AbstractExpressionsRenderer implements IT constructor( @IDebugService protected debugService: IDebugService, @IContextViewService private readonly contextViewService: IContextViewService, + @IHoverService protected readonly hoverService: IHoverService, ) { } abstract get templateId(): string; @@ -194,7 +237,10 @@ export abstract class AbstractExpressionsRenderer implements IT const name = dom.append(expression, $('span.name')); const lazyButton = dom.append(expression, $('span.lazy-button')); lazyButton.classList.add(...ThemeIcon.asClassNameArray(Codicon.eye)); - templateDisposable.add(setupCustomHover(getDefaultHoverDelegate('mouse'), lazyButton, localize('debug.lazyButton.tooltip', "Click to expand"))); + + templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), lazyButton, localize('debug.lazyButton.tooltip', "Click to expand"))); + const type = dom.append(expression, $('span.type')); + const value = dom.append(expression, $('span.value')); const label = templateDisposable.add(new HighlightedLabel(name)); @@ -207,7 +253,7 @@ export abstract class AbstractExpressionsRenderer implements IT actionBar = templateDisposable.add(new ActionBar(expression)); } - const template: IExpressionTemplateData = { expression, name, value, label, inputBoxContainer, actionBar, elementDisposable: new DisposableStore(), templateDisposable, lazyButton, currentElement: undefined }; + const template: IExpressionTemplateData = { expression, name, type, value, label, inputBoxContainer, actionBar, elementDisposable: new DisposableStore(), templateDisposable, lazyButton, currentElement: undefined }; templateDisposable.add(dom.addDisposableListener(lazyButton, dom.EventType.CLICK, () => { if (template.currentElement) { @@ -221,7 +267,6 @@ export abstract class AbstractExpressionsRenderer implements IT public abstract renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void; protected renderExpressionElement(element: IExpression, node: ITreeNode, data: IExpressionTemplateData): void { - data.elementDisposable.clear(); data.currentElement = element; this.renderExpression(node.element, data, createMatches(node.filterData)); if (data.actionBar) { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1b53d40047f..2baa0a915c5 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -504,7 +504,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi if (!clz) { continue; } - const hasSomeActionableCodicon = !(clz.includes('codicon-') || clz.startsWith('coverage-deco-')) || clz.includes('codicon-testing-') || clz.includes('codicon-merge-') || clz.includes('codicon-arrow-') || clz.includes('codicon-loading') || clz.includes('codicon-fold') || clz.includes('codicon-inline-chat'); + const hasSomeActionableCodicon = !(clz.includes('codicon-') || clz.startsWith('coverage-deco-')) || clz.includes('codicon-testing-') || clz.includes('codicon-merge-') || clz.includes('codicon-arrow-') || clz.includes('codicon-loading') || clz.includes('codicon-fold') || clz.includes('codicon-gutter-lightbulb') || clz.includes('codicon-lightbulb-sparkle'); if (hasSomeActionableCodicon) { return false; } @@ -811,66 +811,71 @@ class InlineBreakpointWidget implements IContentWidget, IDisposable { } registerThemingParticipant((theme, collector) => { + const scope = '.monaco-editor .glyph-margin-widgets, .monaco-workbench .debug-breakpoints, .monaco-workbench .disassembly-view'; const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground); if (debugIconBreakpointColor) { - collector.addRule(` - ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']), - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { - color: ${debugIconBreakpointColor} !important; - } - `); + collector.addRule(`${scope} { + ${icons.allBreakpoints.map(b => `${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')}, + ${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)}, + ${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']), + ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, + ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { + color: ${debugIconBreakpointColor} !important; + } + }`); - collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.pending)} { - color: ${debugIconBreakpointColor} !important; - font-size: 12px !important; - } - `); + collector.addRule(`${scope} { + ${ThemeIcon.asCSSSelector(icons.breakpoint.pending)} { + color: ${debugIconBreakpointColor} !important; + font-size: 12px !important; + } + }`); } const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground); if (debugIconBreakpointDisabledColor) { - collector.addRule(` - ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.disabled)}`).join(',\n ')} { - color: ${debugIconBreakpointDisabledColor}; - } - `); + collector.addRule(`${scope} { + ${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.disabled)).join(',\n ')} { + color: ${debugIconBreakpointDisabledColor}; + } + }`); } const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground); if (debugIconBreakpointUnverifiedColor) { - collector.addRule(` - ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.unverified)}`).join(',\n ')} { - color: ${debugIconBreakpointUnverifiedColor}; - } - `); + collector.addRule(`${scope} { + ${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.unverified)).join(',\n ')} { + color: ${debugIconBreakpointUnverifiedColor}; + } + }`); } const debugIconBreakpointCurrentStackframeForegroundColor = theme.getColor(debugIconBreakpointCurrentStackframeForeground); if (debugIconBreakpointCurrentStackframeForegroundColor) { collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStackframe)}, .monaco-editor .debug-top-stack-frame-column { color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important; } + ${scope} { + ${ThemeIcon.asCSSSelector(icons.debugStackframe)} { + color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important; + } + } `); } const debugIconBreakpointStackframeFocusedColor = theme.getColor(debugIconBreakpointStackframeForeground); if (debugIconBreakpointStackframeFocusedColor) { - collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)} { - color: ${debugIconBreakpointStackframeFocusedColor} !important; - } - `); + collector.addRule(`${scope} { + ${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)} { + color: ${debugIconBreakpointStackframeFocusedColor} !important; + } + }`); } }); -export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', { dark: '#E51400', light: '#E51400', hcDark: '#E51400', hcLight: '#E51400' }, nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); -const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); -const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); +export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', '#E51400', nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); +const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', '#848484', nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); +const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', '#848484', nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#BE8700', hcDark: '#FFCC00', hcLight: '#BE8700' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.')); -const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#89D185', light: '#89D185', hcDark: '#89D185', hcLight: '#89D185' }, nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); +const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', '#89D185', nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 59a3a4dd4bf..c002c37848f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -6,6 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Button } from 'vs/base/browser/ui/button/button'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -34,6 +35,7 @@ import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -109,6 +111,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi @IKeybindingService private readonly keybindingService: IKeybindingService, @ILabelService private readonly labelService: ILabelService, @ITextModelService private readonly textModelService: ITextModelService, + @IHoverService private readonly hoverService: IHoverService ) { super(editor, { showFrame: true, showArrow: false, frameWidth: 1, isAccessible: true }); @@ -204,12 +207,12 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi protected _fillContainer(container: HTMLElement): void { this.setCssClass('breakpoint-widget'); - const selectBox = new SelectBox([ + const selectBox = new SelectBox([ { text: nls.localize('expression', "Expression") }, { text: nls.localize('hitCount', "Hit Count") }, { text: nls.localize('logMessage', "Log Message") }, { text: nls.localize('triggeredBy', "Wait for Breakpoint") }, - ], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); + ] satisfies ISelectOptionItem[], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); this.selectContainer = $('.breakpoint-select-container'); selectBox.render(dom.append(container, this.selectContainer)); selectBox.onDidSelect(e => { @@ -221,6 +224,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.createModesInput(container); this.inputContainer = $('.inputContainer'); + this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.inputContainer, this.placeholder)); this.createBreakpointInput(dom.append(container, this.inputContainer)); this.input.getModel().setValue(this.getInputValue(this.breakpoint)); @@ -264,7 +268,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } private createTriggerBreakpointInput(container: HTMLElement) { - const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint); + const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint && !bp.logMessage); const breakpointOptions: ISelectOptionItem[] = [ { text: nls.localize('noTriggerByBreakpoint', 'None'), isDisabled: true }, ...breakpoints.map(bp => ({ @@ -346,7 +350,10 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.toDispose.push(scopedContextKeyService); const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection( - [IContextKeyService, scopedContextKeyService], [IPrivateBreakpointWidgetService, this])); + [IContextKeyService, scopedContextKeyService], + [IPrivateBreakpointWidgetService, this] + )); + this.toDispose.push(scopedInstatiationService); const options = this.createEditorOptions(); const codeEditorWidgetOptions = getSimpleCodeEditorWidgetOptions(); @@ -433,12 +440,12 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi if (success) { // if there is already a breakpoint on this location - remove it. - let condition = this.breakpoint?.condition; - let hitCondition = this.breakpoint?.hitCondition; - let logMessage = this.breakpoint?.logMessage; - let triggeredBy = this.breakpoint?.triggeredBy; - let mode = this.breakpoint?.mode; - let modeLabel = this.breakpoint?.modeLabel; + let condition: string | undefined = undefined; + let hitCondition: string | undefined = undefined; + let logMessage: string | undefined = undefined; + let triggeredBy: string | undefined = undefined; + let mode: string | undefined = undefined; + let modeLabel: string | undefined = undefined; this.rememberInput(); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index ddfb63625fd..ac8dc3ed3d0 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -10,7 +10,6 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IListContextMenuEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -56,6 +55,7 @@ import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, In import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; const $ = dom.$; @@ -114,10 +114,10 @@ export class BreakpointsView extends ViewPane { @ITelemetryService telemetryService: ITelemetryService, @ILabelService private readonly labelService: ILabelService, @IMenuService menuService: IMenuService, - @IHoverService private readonly hoverService: IHoverService, + @IHoverService hoverService: IHoverService, @ILanguageService private readonly languageService: ILanguageService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); this._register(this.menu); @@ -141,21 +141,19 @@ export class BreakpointsView extends ViewPane { this.list = this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType), - new ExceptionBreakpointsRenderer(this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.debugService), + new ExceptionBreakpointsRenderer(this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.debugService, this.hoverService), new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), - new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), + new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.hoverService, this.labelService), this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.breakpointIsDataBytes), - new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), + new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.hoverService, this.labelService), this.instantiationService.createInstance(InstructionBreakpointsRenderer), ], { identityProvider: { getId: (element: IEnablement) => element.getId() }, multipleSelectionSupport: false, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IEnablement) => e }, accessibilityProvider: new BreakpointsAccessibilityProvider(this.debugService, this.labelService), - overrideStyles: { - listBackground: this.getBackgroundColor() - } + overrideStyles: this.getLocationBasedColors().listOverrideStyles }) as WorkbenchList; CONTEXT_BREAKPOINTS_FOCUSED.bindTo(this.list.contextKeyService); @@ -499,6 +497,7 @@ class BreakpointsRenderer implements IListRenderer, private breakpointItemType: IContextKey, @IDebugService private readonly debugService: IDebugService, + @IHoverService private readonly hoverService: IHoverService, @ILabelService private readonly labelService: ILabelService ) { // noop @@ -555,7 +554,7 @@ class BreakpointsRenderer implements IListRenderer, private breakpointSupportsCondition: IContextKey, private breakpointItemType: IContextKey, - private debugService: IDebugService + private debugService: IDebugService, + private readonly hoverService: IHoverService, ) { // noop } @@ -624,11 +624,11 @@ class ExceptionBreakpointsRenderer implements IListRenderer, private breakpointItemType: IContextKey, @IDebugService private readonly debugService: IDebugService, + @IHoverService private readonly hoverService: IHoverService, @ILabelService private readonly labelService: ILabelService ) { // noop @@ -700,9 +701,9 @@ class FunctionBreakpointsRenderer implements IListRenderer, private breakpointIsDataBytes: IContextKey, @IDebugService private readonly debugService: IDebugService, + @IHoverService private readonly hoverService: IHoverService, @ILabelService private readonly labelService: ILabelService ) { // noop @@ -788,9 +790,9 @@ class DataBreakpointsRenderer implements IListRenderer { const debugService = accessor.get(IDebugService); + const viewService = accessor.get(IViewsService); + await viewService.openView(BREAKPOINTS_VIEW_ID); debugService.addFunctionBreakpoint(); } }); @@ -1639,7 +1646,7 @@ registerAction2(class extends Action2 { } else if (breakpoint instanceof DataBreakpoint) { await debugService.removeDataBreakpoints(breakpoint.getId()); } else if (breakpoint instanceof InstructionBreakpoint) { - await debugService.removeInstructionBreakpoints(breakpoint.instructionReference); + await debugService.removeInstructionBreakpoints(breakpoint.instructionReference, breakpoint.offset); } } }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 6c3aeb1d666..70240432101 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -48,19 +48,28 @@ const FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { color: themeColorFromId(focusedStackFrameColor) } }; -const TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { +export const TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { description: 'top-stack-frame-decoration', isWholeLine: true, className: 'debug-top-stack-frame-line', stickiness }; -const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { +export const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { description: 'focused-stack-frame-decoration', isWholeLine: true, className: 'debug-focused-stack-frame-line', stickiness }; +export const makeStackFrameColumnDecoration = (noCharactersBefore: boolean): IModelDecorationOptions => ({ + description: 'top-stack-frame-inline-decoration', + before: { + content: '\uEB8B', + inlineClassName: noCharactersBefore ? 'debug-top-stack-frame-column start-of-line' : 'debug-top-stack-frame-column', + inlineClassNameAffectsLetterSpacing: true + }, +}); + export function createDecorationsForStackFrame(stackFrame: IStackFrame, isFocusedSession: boolean, noCharactersBefore: boolean): IModelDeltaDecoration[] { // only show decorations for the currently focused thread. const result: IModelDeltaDecoration[] = []; @@ -85,14 +94,7 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, isFocuse if (stackFrame.range.startColumn > 1) { result.push({ - options: { - description: 'top-stack-frame-inline-decoration', - before: { - content: '\uEB8B', - inlineClassName: noCharactersBefore ? 'debug-top-stack-frame-column start-of-line' : 'debug-top-stack-frame-column', - inlineClassNameAffectsLetterSpacing: true - }, - }, + options: makeStackFrameColumnDecoration(noCharactersBefore), range: columnUntilEOLRange }); } diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index db948476aef..6ee016a167c 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -45,11 +45,12 @@ import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { createDisconnectMenuItemAction } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, IStackFrame, IThread, State } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, isFrameDeemphasized, IStackFrame, IThread, State } from 'vs/workbench/contrib/debug/common/debug'; import { StackFrame, Thread, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = dom.$; @@ -135,7 +136,7 @@ async function expandTo(session: IDebugSession, tree: WorkbenchCompressibleAsync export class CallStackView extends ViewPane { private stateMessage!: HTMLSpanElement; private stateMessageLabel!: HTMLSpanElement; - private stateMessageLabelHover!: ICustomHover; + private stateMessageLabelHover!: IManagedHover; private onCallStackChangeScheduler: RunOnceScheduler; private needsRefresh = false; private ignoreSelectionChangedEvent = false; @@ -158,9 +159,10 @@ export class CallStackView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IHoverService hoverService: IHoverService, @IMenuService private readonly menuService: IMenuService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); // Create scheduler to prevent unnecessary flashing of tree when reacting to changes this.onCallStackChangeScheduler = this._register(new RunOnceScheduler(async () => { @@ -219,7 +221,7 @@ export class CallStackView extends ViewPane { this.stateMessage = dom.append(container, $('span.call-stack-state-message')); this.stateMessage.hidden = true; this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); - this.stateMessageLabelHover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.stateMessage, '')); + this.stateMessageLabelHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.stateMessage, '')); } protected override renderBody(container: HTMLElement): void { @@ -233,7 +235,7 @@ export class CallStackView extends ViewPane { this.instantiationService.createInstance(SessionsRenderer), this.instantiationService.createInstance(ThreadsRenderer), this.instantiationService.createInstance(StackFramesRenderer), - new ErrorsRenderer(), + this.instantiationService.createInstance(ErrorsRenderer), new LoadMoreRenderer(), new ShowMoreRenderer() ], this.dataSource, { @@ -278,9 +280,7 @@ export class CallStackView extends ViewPane { } }, expandOnlyOnTwistieClick: true, - overrideStyles: { - listBackground: this.getBackgroundColor() - } + overrideStyles: this.getLocationBasedColors().listOverrideStyles }); this.tree.setInput(this.debugService.getModel()); @@ -465,9 +465,8 @@ export class CallStackView extends ViewPane { const secondary: IAction[] = []; const result = { primary, secondary }; const contextKeyService = this.contextKeyService.createOverlay(overlay); - const menu = this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); - createAndFillInContextMenuActions(menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, 'inline'); - menu.dispose(); + const menu = this.menuService.getMenuActions(MenuId.DebugCallStackContext, contextKeyService, { arg: getContextForContributedActions(element), shouldForwardArgs: true }); + createAndFillInContextMenuActions(menu, result, 'inline'); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => result.secondary, @@ -530,6 +529,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer { if ((action.id === STOP_ID || action.id === DISCONNECT_ID) && action instanceof MenuItemAction) { stopActionViewItemDisposables.clear(); - const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, stopActionViewItemDisposables, accessor, options)); + const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, stopActionViewItemDisposables, accessor, { ...options, menuAsChild: false })); if (item) { return item; } @@ -581,7 +581,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer t.stopped); @@ -646,6 +646,7 @@ class ThreadsRenderer implements ICompressibleTreeRenderer, _index: number, data: IThreadTemplateData): void { const thread = element.element; - data.elementDisposable.add(setupCustomHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name)); + data.elementDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name)); data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception'); @@ -718,6 +719,7 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, index: number, data: IStackFrameTemplateData): void { const stackFrame = element.element; - data.stackFrame.classList.toggle('disabled', !stackFrame.source || !stackFrame.source.available || isDeemphasized(stackFrame)); + data.stackFrame.classList.toggle('disabled', !stackFrame.source || !stackFrame.source.available || isFrameDeemphasized(stackFrame)); data.stackFrame.classList.toggle('label', stackFrame.presentationHint === 'label'); - data.stackFrame.classList.toggle('subtle', stackFrame.presentationHint === 'subtle'); const hasActions = !!stackFrame.thread.session.capabilities.supportsRestartFrame && stackFrame.presentationHint !== 'label' && stackFrame.presentationHint !== 'subtle' && stackFrame.canRestart; data.stackFrame.classList.toggle('has-actions', hasActions); @@ -754,7 +755,7 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, index: number, data: IErrorTemplateData): void { const error = element.element; data.label.textContent = error; - data.templateDisposable.add(setupCustomHover(getDefaultHoverDelegate('mouse'), data.label, error)); + data.templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.label, error)); } renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IErrorTemplateData, height: number | undefined): void { @@ -933,10 +939,6 @@ function isDebugSession(obj: any): obj is IDebugSession { return obj && typeof obj.getAllThreads === 'function'; } -function isDeemphasized(frame: IStackFrame): boolean { - return frame.source.presentationHint === 'deemphasize' || frame.presentationHint === 'deemphasize'; -} - class CallStackDataSource implements IAsyncDataSource { deemphasizedStackFramesToShow: IStackFrame[] = []; @@ -984,7 +986,7 @@ class CallStackDataSource implements IAsyncDataSource
    +
  1. hello test text
  2. +
+